Skip to main content

libxml_rs/abi/
exports_xslt_compile.rs

1//! C ABI exports for libxslt.so.1 — the "compile" family (§16, Phase 8).
2//!
3//! This module implements the stylesheet-compilation entry points of the
4//! libxslt 1.1.45 C ABI:
5//!
6//! - Stylesheet creation: `xsltNewStylesheet`, `xsltParseStylesheetProcess`,
7//!   `xsltParseStylesheetUser`, `xsltParseStylesheetImportedDoc`
8//! - Imports/includes: `xsltParseStylesheetImport`, `xsltParseStylesheetInclude`
9//! - Top-level constructs: `xsltParseStylesheetOutput`,
10//!   `xsltParseStylesheetAttributeSet`, `xsltParseGlobalVariable`,
11//!   `xsltParseGlobalParam`
12//! - Content preprocessing: `xsltParseTemplateContent`, `xsltCompileAttr`
13//! - Precomputed instructions: `xsltDocumentComp`, `xsltStylePreCompute`,
14//!   `xsltPreComputeExtModuleElement`, `xsltNormalizeCompSteps`,
15//!   `xsltFreeStylePreComps`
16//! - Style documents: `xsltNewStyleDocument`, `xsltLoadStyleDocument`,
17//!   `xsltFreeStyleDocuments`
18//! - Global state: `xsltInitGlobals`, `xsltUninit`, `xsltFreeExts`,
19//!   `xsltShutdownExts`, `xsltDebugDumpExtensions`
20//!
21//! # UPSTREAM-PARITY
22//!
23//! Every function is a faithful port of the upstream libxslt 1.1.45 sources
24//! in `archaeology/libxslt-git/libxslt/` (xslt.c, imports.c, preproc.c,
25//! attributes.c, attrvt.c, documents.c, variables.c, extensions.c, pattern.c).
26//! The oracle build has `XSLT_REFACTORED` disabled, so the *old* (non
27//! refactored) code paths are the authoritative semantics.
28//!
29//! # Engine wiring
30//!
31//! The native-Rust engine in `src/xslt/compiler` compiles stylesheets
32//! eagerly (top-level constructs) but compiles *instructions* lazily: at
33//! transform time the runtime dispatches on the raw instruction node
34//! (`src/xslt/transform`, `xsltProcessInstruction`) and never consults
35//! `node->psvi`. Consequently the upstream per-instruction compilers
36//! (`xsltApplyTemplatesComp` et al.) have no data to store; the ABI
37//! functions below keep their *observable* semantics (the grammar checks
38//! that bump `style->errors` / `style->warnings`, the return values, the
39//! `style->preComps` chain for the structures that genuinely exist) and
40//! skip the dead precomp allocation. Each such divergence is documented
41//! at the function.
42
43#![allow(non_snake_case)]
44#![allow(unused_variables)]
45#![allow(clippy::missing_safety_doc)]
46#![allow(clippy::not_unsafe_ptr_arg_deref)]
47
48use core::ffi::c_void;
49use core::ptr;
50use std::os::raw::{c_char, c_int};
51
52use crate::abi::allocator::{xmlFreeImpl, xmlMallocImpl};
53use crate::abi::exports_hash::xmlDictReference;
54use crate::abi::exports_string::xmlStrstr;
55use crate::abi::exports_tree::xmlNodeGetBase;
56use crate::abi::exports_uri::xmlBuildURI;
57use crate::abi::exports_xml2::*;
58use crate::abi::structs::*;
59use crate::abi::types::xmlElementType::*;
60use crate::abi::types::*;
61use crate::xml::xpath::exports::xmlXPathNewString;
62
63/// The XSLT namespace URI (upstream `XSLT_NAMESPACE`, xslt.h).
64const XSLT_NAMESPACE: &[u8] = b"http://www.w3.org/1999/XSL/Transform";
65
66/// `XSLT_PARSE_OPTIONS` (xslt.h): NOENT | DTDLOAD | DTDATTR | NOCDATA.
67const XSLT_PARSE_OPTIONS: c_int = (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4);
68
69/// `XSLT_LOAD_STYLESHEET` (documents.h).
70const XSLT_LOAD_STYLESHEET: c_int = 1;
71
72/// `xsltStyleType` values (xsltInternals.h, non-refactored enum).
73const XSLT_FUNC_DOCUMENT: c_int = 17;
74const XSLT_FUNC_EXTENSION: c_int = 22;
75
76/// `XSLT_VAR_PARAM` (variables.c): stack-elem PARAM flag used to
77/// distinguish global variables from parameters in `_xsltStackElem.flags`.
78const XSLT_VAR_PARAM: c_int = 1 << 1;
79
80/// `XSLT_SECPREF_READ_FILE` / `XSLT_SECPREF_READ_NETWORK` (security.c).
81const XSLT_SECPREF_READ_FILE: c_int = 1;
82const XSLT_SECPREF_READ_NETWORK: c_int = 4;
83
84/// `XSLT_MAX_NESTING` (imports.c).
85const XSLT_MAX_NESTING: c_int = 40;
86
87/// `xsltExtMarker` (preproc.c) — the sentinel stored in `inst->psvi` for
88/// extension elements with no registered precomputation.
89///
90/// # UPSTREAM-PARITY
91///
92/// Upstream exports `xsltExtMarker` as a variable; the candidate engine
93/// never reads `psvi`, so the marker is carried as a private static (the
94/// ext-family ABI exports own the exported variable).
95static XSLT_EXT_MARKER: [u8; 18] = *b"Extension Element\0";
96
97// ═══════════════════════════════════════════════════════════════════════════════
98// Types & structures
99// ═══════════════════════════════════════════════════════════════════════════════
100
101/// `xsltTransformFunction` (xsltInternals.h): the handling function of a
102/// compiled instruction/extension element.
103pub type xsltTransformFunction = unsafe extern "C" fn(
104    ctxt: *mut _xsltTransformContext,
105    node: *mut _xmlNode,
106    inst: *mut _xmlNode,
107    comp: *mut c_void,
108);
109
110/// `xsltElemPreCompDeallocator` (xsltInternals.h): deallocates a precomp.
111pub type xsltElemPreCompDeallocator = unsafe extern "C" fn(comp: *mut c_void);
112
113/// `xsltPreComputeFunction` (extensions.h): precomputation callback of an
114/// extension element.
115pub type xsltPreComputeFunction = unsafe extern "C" fn(
116    style: *mut _xsltStylesheet,
117    inst: *mut _xmlNode,
118    function: Option<xsltTransformFunction>,
119) -> *mut c_void;
120
121/// `_xsltElemPreComp` (xsltInternals.h, non-refactored layout).
122///
123/// ```c
124/// struct _xsltElemPreComp {
125///     xsltElemPreCompPtr next;    /* next item in the global chained list
126///                                    held by xsltStylesheet. */
127///     xsltStyleType type;         /* type of the element */
128///     xsltTransformFunction func; /* handling function */
129///     xmlNodePtr inst;            /* the node in the stylesheet's tree
130///                                    corresponding to this item */
131///     /* end of common part */
132///     xsltElemPreCompDeallocator free; /* the deallocator */
133/// };
134/// ```
135#[repr(C)]
136pub struct _xsltElemPreComp {
137    pub next: *mut _xsltElemPreComp,
138    pub type_: c_int, // xsltStyleType
139    pub func: Option<xsltTransformFunction>,
140    pub inst: *mut _xmlNode,
141    pub free: Option<xsltElemPreCompDeallocator>,
142}
143
144/// The old (non-refactored) `_xsltStylePreComp` (xsltInternals.h) extends
145/// `_xsltElemPreComp` with per-instruction precomputed values. The
146/// candidate engine compiles instructions lazily, so only the fields that
147/// the compile-family itself writes (`ver11`, `filename`, `has_filename`,
148/// used by `xsltDocumentComp`) are carried; the remaining upstream fields
149/// (sort/name/select/numdata/comp/nsList…) hold nothing in this engine and
150/// are omitted (documented divergence — nothing reads them).
151#[repr(C)]
152struct _xsltStylePreComp {
153    pub base: _xsltElemPreComp,
154    pub ver11: c_int,
155    pub filename: *const xmlChar,
156    pub has_filename: c_int,
157}
158
159/// Extension-element registry entry (upstream `xsltElementsHash` payload
160/// `_xsltExtElement { precomp, transform }`, extended with the lookup key).
161#[repr(C)]
162struct _xsltExtElementEntry {
163    pub next: *mut _xsltExtElementEntry,
164    pub name: *mut xmlChar,
165    pub URI: *mut xmlChar,
166    pub precomp: Option<xsltPreComputeFunction>,
167    pub transform: Option<xsltTransformFunction>,
168}
169
170/// Global registry of registered extension elements, keyed by
171/// `(name, namespace-URI)` — the candidate mirror of upstream's global
172/// `xsltElementsHash`. Upstream guards it with `xsltExtMutex`; the
173/// candidate build is single-threaded for the compile phase, matching the
174/// rest of the crate's registry handling.
175static mut XSLT_ELEMENTS_REGISTRY: *mut _xsltExtElementEntry = ptr::null_mut();
176
177/// Global registry of registered extension *modules* — the candidate
178/// mirror of upstream's `xsltExtensionsHash` (used by
179/// `xsltDebugDumpExtensions` and `xsltShutdownExts`).
180#[repr(C)]
181struct _xsltExtModuleEntry {
182    pub next: *mut _xsltExtModuleEntry,
183    pub URI: *mut xmlChar,
184    pub shutdownFunc: Option<unsafe extern "C" fn(*mut c_void, *const xmlChar, *mut c_void)>,
185}
186
187static mut XSLT_MODULES_REGISTRY: *mut _xsltExtModuleEntry = ptr::null_mut();
188
189/// Whether `xsltInitGlobals` has run (mirrors upstream `xsltExtMutex !=
190/// NULL`).
191static mut XSLT_GLOBALS_INITIALIZED: c_int = 0;
192
193// ═══════════════════════════════════════════════════════════════════════════════
194// Helpers
195// ═══════════════════════════════════════════════════════════════════════════════
196
197/// IS_XSLT_ELEM (xsltutils.h).
198unsafe fn is_xslt_elem(n: *mut _xmlNode) -> bool {
199    if n.is_null() || (*n).type_ != XML_ELEMENT_NODE as c_int || (*n).ns.is_null() {
200        return false;
201    }
202    xmlStrEqual((*(*n).ns).href, XSLT_NAMESPACE.as_ptr() as *const xmlChar) != 0
203}
204
205/// IS_XSLT_NAME (xsltutils.h).
206unsafe fn is_xslt_name(n: *mut _xmlNode, val: &[u8]) -> bool {
207    if n.is_null() || (*n).name.is_null() {
208        return false;
209    }
210    let len = libc::strlen((*n).name as *const libc::c_char) as usize;
211    len == val.len() && core::slice::from_raw_parts((*n).name, len) == val
212}
213
214/// IS_BLANK (xsltutils.h): a string made only of XML whitespace.
215unsafe fn is_blank_str(str: *const xmlChar) -> bool {
216    if str.is_null() {
217        return true;
218    }
219    let mut cur = str;
220    while *cur != 0 {
221        if *cur != b' ' && *cur != b'\t' && *cur != b'\n' && *cur != b'\r' {
222            return false;
223        }
224        cur = cur.add(1);
225    }
226    true
227}
228
229/// Report a compile-time error (xsltTransformError; the candidate records
230/// the literal message, matching the crate's non-variadic convention).
231/// Render a NUL-terminated C string as a byte slice for `report_error`.
232unsafe fn cbytes(p: *const u8) -> &'static [u8] {
233    if p.is_null() {
234        return b"";
235    }
236    core::ffi::CStr::from_ptr(p as *const c_char).to_bytes()
237}
238
239unsafe fn report_error(style: *mut _xsltStylesheet, inst: *mut _xmlNode, msg: &[u8]) {
240    let mut m = msg.to_vec();
241    m.push(0);
242    crate::xslt::errors::xsltTransformError(
243        ptr::null_mut(),
244        style,
245        inst,
246        m.as_ptr() as *const c_char,
247    );
248}
249
250/// `xsltFreeExtDef` (extensions.c): free one extension-prefix def.
251///
252/// Not used by the compile family itself (see `xsltFreeExts`), but kept
253/// for symmetry with the upstream def-list handling.
254#[allow(dead_code)]
255unsafe fn xslt_free_ext_def(entry: *mut c_void) {
256    // The candidate never allocates xsltExtDef lists; this is unreachable
257    // and kept only to document the upstream shape.
258    let _ = entry;
259}
260
261/// Look up a registered extension element by (name, namespace-URI).
262unsafe fn ext_element_lookup(
263    name: *const xmlChar,
264    uri: *const xmlChar,
265) -> *mut _xsltExtElementEntry {
266    if name.is_null() || uri.is_null() {
267        return ptr::null_mut();
268    }
269    let mut cur = XSLT_ELEMENTS_REGISTRY;
270    while !cur.is_null() {
271        if !(*cur).name.is_null()
272            && !(*cur).URI.is_null()
273            && xmlStrEqual((*cur).name, name) != 0
274            && xmlStrEqual((*cur).URI, uri) != 0
275        {
276            return cur;
277        }
278        cur = (*cur).next;
279    }
280    ptr::null_mut()
281}
282
283/// Duplicate a NUL-terminated string with the xml allocator.
284unsafe fn dup_str(s: *const xmlChar) -> *mut xmlChar {
285    if s.is_null() {
286        return ptr::null_mut();
287    }
288    let len = libc::strlen(s as *const libc::c_char);
289    let copy = xmlMallocImpl(len + 1) as *mut xmlChar;
290    if copy.is_null() {
291        return ptr::null_mut();
292    }
293    core::ptr::copy_nonoverlapping(s, copy, len);
294    *copy.add(len) = 0;
295    copy
296}
297
298/// `xsltCheckRead` (security.c) for the file/network split. The candidate
299/// security module exposes only the check-fn registry; the URI-scheme
300/// analysis is reduced to upstream's first-order file-vs-network test
301/// (a `://` in the value selects the network check).
302///
303/// Returns 1 if read is allowed, 0 if denied, -1 on error.
304unsafe fn xslt_check_read(
305    sec: *mut c_void,
306    ctxt: *mut _xsltTransformContext,
307    url: *const xmlChar,
308) -> c_int {
309    if sec.is_null() {
310        return 1;
311    }
312    let is_network = !xmlStrstr(url, b"://\0".as_ptr() as *const xmlChar).is_null();
313    let option = if is_network {
314        XSLT_SECPREF_READ_NETWORK
315    } else {
316        XSLT_SECPREF_READ_FILE
317    };
318    let check = crate::xslt::security::xsltGetSecurityPrefs(sec, option);
319    if let Some(check_fn) = check {
320        let ret = check_fn(sec, ctxt as *mut c_void, url as *const c_char);
321        if ret == 0 {
322            if is_network {
323                report_error(ptr::null_mut(), ptr::null_mut(), b"Network access for ");
324            } else {
325                report_error(ptr::null_mut(), ptr::null_mut(), b"Local file read for ");
326            }
327            report_error(ptr::null_mut(), ptr::null_mut(), cbytes(url as *const u8));
328            report_error(ptr::null_mut(), ptr::null_mut(), b" refused\n");
329            return 0;
330        }
331        return ret;
332    }
333    1
334}
335
336/// `xsltDocDefaultLoader` (documents.c) for the candidate engine: if a
337/// global loader function is registered it is invoked (the returned
338/// parser input cannot be fed into a document by this engine and is
339/// freed, matching `src/xslt/documents` `load_via_loader`); otherwise the
340/// URI is parsed as a file. Returns a parsed document or NULL.
341unsafe fn xslt_doc_default_loader(
342    uri: *const xmlChar,
343    _dict: *mut c_void,
344    options: c_int,
345    ctxt: *mut c_void,
346    _type: c_int,
347) -> *mut _xmlDoc {
348    let loader = crate::xslt::documents::xsltGetLoaderFunc();
349    if let Some(loader_fn) = loader {
350        let input = loader_fn(
351            ctxt,
352            ptr::null(), // base URL
353            uri as *const c_char,
354            ptr::null(), // ns
355            0,           // secondary
356        );
357        if !input.is_null() {
358            crate::xml::parser::helpers::free_parser_input(input);
359        }
360    }
361    xmlReadFile(uri as *const c_char, ptr::null(), options)
362}
363
364/// `xsltNewDecimalFormat` (xslt.c): create a decimal format with the
365/// default values. `name`/`nsUri` are borrowed (not owned).
366unsafe fn xslt_new_decimal_format(
367    nsUri: *const xmlChar,
368    name: *mut xmlChar,
369) -> *mut _xsltDecimalFormat {
370    let self_ = xmlMallocImpl(core::mem::size_of::<_xsltDecimalFormat>()) as *mut _xsltDecimalFormat;
371    if !self_.is_null() {
372        ptr::write_bytes(
373            self_ as *mut u8,
374            0,
375            core::mem::size_of::<_xsltDecimalFormat>(),
376        );
377        (*self_).nsUri = nsUri;
378        (*self_).name = name;
379        // Default values (xslt.c, UTF-8 for U+2030 PER MILLE SIGN).
380        (*self_).digit = xmlStrdup(b"#\0".as_ptr() as *const xmlChar);
381        (*self_).patternSeparator = xmlStrdup(b";\0".as_ptr() as *const xmlChar);
382        (*self_).decimalPoint = xmlStrdup(b".\0".as_ptr() as *const xmlChar);
383        (*self_).grouping = xmlStrdup(b",\0".as_ptr() as *const xmlChar);
384        (*self_).percent = xmlStrdup(b"%\0".as_ptr() as *const xmlChar);
385        (*self_).permille = xmlStrdup("\u{2030}\0".as_ptr() as *const xmlChar);
386        (*self_).zeroDigit = xmlStrdup(b"0\0".as_ptr() as *const xmlChar);
387        (*self_).minusSign = xmlStrdup(b"-\0".as_ptr() as *const xmlChar);
388        (*self_).infinity = xmlStrdup(b"Infinity\0".as_ptr() as *const xmlChar);
389        (*self_).noNumber = xmlStrdup(b"NaN\0".as_ptr() as *const xmlChar);
390    }
391    self_
392}
393
394/// `xsltNewStylesheetInternal` (xslt.c).
395unsafe fn xslt_new_stylesheet_internal(parent: *mut _xsltStylesheet) -> *mut _xsltStylesheet {
396    let ret = xmlMallocImpl(core::mem::size_of::<_xsltStylesheet>()) as *mut _xsltStylesheet;
397    if ret.is_null() {
398        report_error(
399            ptr::null_mut(),
400            ptr::null_mut(),
401            b"xsltNewStylesheet : malloc failed\n",
402        );
403        return ptr::null_mut();
404    }
405    ptr::write_bytes(ret as *mut u8, 0, core::mem::size_of::<_xsltStylesheet>());
406
407    (*ret).parent = parent;
408    (*ret).omitXmlDeclaration = -1;
409    (*ret).standalone = -1;
410    (*ret).decimalFormat = xslt_new_decimal_format(ptr::null(), ptr::null_mut());
411    (*ret).indent = -1;
412    (*ret).errors = 0;
413    (*ret).warnings = 0;
414    (*ret).exclPrefixNr = 0;
415    (*ret).exclPrefixMax = 0;
416    (*ret).exclPrefixTab = ptr::null_mut();
417    (*ret).extInfos = ptr::null_mut();
418    (*ret).extrasNr = 0;
419    (*ret).internalized = 1;
420    (*ret).literal_result = 0;
421    (*ret).forwards_compatible = 0;
422    (*ret).dict = xmlDictCreate();
423
424    if parent.is_null() {
425        (*ret).principal = ret;
426        (*ret).xpathCtxt = xmlXPathNewContext(ptr::null_mut());
427        if (*ret).xpathCtxt.is_null() {
428            report_error(
429                ptr::null_mut(),
430                ptr::null_mut(),
431                b"xsltNewStylesheet: xmlXPathNewContext failed\n",
432            );
433            crate::xslt::stylesheet::xsltFreeStylesheet(ret);
434            return ptr::null_mut();
435        }
436        if crate::xml::xpath::exports::xmlXPathContextSetCache((*ret).xpathCtxt, 1, -1, 0) == -1 {
437            crate::xslt::stylesheet::xsltFreeStylesheet(ret);
438            return ptr::null_mut();
439        }
440    } else {
441        (*ret).principal = (*parent).principal;
442    }
443
444    // Upstream calls xsltInit() (registers built-in extras, sets the
445    // initialized flag). The candidate has no built-in extras; xsltInit
446    // only marks the library initialized.
447    crate::abi::exports_xslt::xsltInit();
448
449    ret
450}
451
452// ═══════════════════════════════════════════════════════════════════════════════
453// 1. Stylesheet creation & parsing (xslt.c)
454// ═══════════════════════════════════════════════════════════════════════════════
455
456/// Create a new XSLT stylesheet.
457///
458/// # UPSTREAM-PARITY
459///
460/// ```c
461/// xsltStylesheetPtr
462/// xsltNewStylesheet(void) {
463///     return xsltNewStylesheetInternal(NULL);
464/// }
465/// ```
466///
467/// See `xsltNewStylesheetInternal` (xslt.c 1.1.45): xmlMalloc + memset, a
468/// default decimal format, `dict = xmlDictCreate()`, `internalized = 1`,
469/// and for the principal stylesheet an XPath context with its cache
470/// enabled. `version`/`method`/`encoding` are left NULL (they are set
471/// later by `xsltParseStylesheetOutput`/version processing).
472///
473/// # SAFETY
474///
475/// The caller owns the returned stylesheet and must free it with
476/// `xsltFreeStylesheet`.
477#[no_mangle]
478pub unsafe extern "C" fn xsltNewStylesheet() -> *mut _xsltStylesheet {
479    xslt_new_stylesheet_internal(ptr::null_mut())
480}
481
482/// Parse an XSLT stylesheet, adding the associated structures.
483///
484/// # UPSTREAM-PARITY
485///
486/// ```c
487/// xsltStylesheetPtr
488/// xsltParseStylesheetProcess(xsltStylesheetPtr ret, xmlDocPtr doc) {
489///     xsltInitGlobals();
490///     if (doc == NULL) return(NULL);
491///     if (ret == NULL) return(ret);
492///     cur = xmlDocGetRootElement(doc);
493///     if (cur == NULL) { ... "empty stylesheet" ... return(NULL); }
494///     ...
495/// }
496/// ```
497///
498/// # ENGINE-WIRING
499///
500/// The heavy lifting (tree preprocessing, top-level compilation, or the
501/// simplified-stylesheet implicit template) is performed by the engine's
502/// `crate::xslt::compiler::compile`, which returns 0 on success.
503///
504/// # SAFETY
505///
506/// - `style` must be a valid `_xsltStylesheet`, or NULL.
507/// - `doc` must be a valid parsed document, or NULL.
508#[no_mangle]
509pub unsafe extern "C" fn xsltParseStylesheetProcess(
510    style: *mut _xsltStylesheet,
511    doc: *mut _xmlDoc,
512) -> *mut _xsltStylesheet {
513    xsltInitGlobals();
514
515    if doc.is_null() {
516        return ptr::null_mut();
517    }
518    if style.is_null() {
519        return style;
520    }
521
522    let root = crate::xml::tree::doc_get_root_element(doc);
523    if root.is_null() {
524        report_error(
525            style,
526            doc as *mut _xmlNode,
527            b"xsltParseStylesheetProcess : empty stylesheet\n",
528        );
529        return ptr::null_mut();
530    }
531
532    let ret = crate::xslt::compiler::compile(style, doc);
533    if ret != 0 {
534        return ptr::null_mut();
535    }
536    style
537}
538
539/// Parse an XSLT stylesheet with a user-provided stylesheet struct.
540///
541/// # UPSTREAM-PARITY
542///
543/// ```c
544/// int
545/// xsltParseStylesheetUser(xsltStylesheetPtr style, xmlDocPtr doc) {
546///     if ((style == NULL) || (doc == NULL)) return(-1);
547///     if (doc->dict != NULL) {
548///         xmlDictFree(style->dict);
549///         style->dict = doc->dict;
550///         xmlDictReference(style->dict);
551///     }
552///     xsltGatherNamespaces(style);
553///     style->doc = doc;
554///     if (xsltParseStylesheetProcess(style, doc) == NULL) {
555///         style->doc = NULL;
556///         return(-1);
557///     }
558///     if (style->parent == NULL)
559///         xsltResolveStylesheetAttributeSet(style);
560///     if (style->errors != 0) {
561///         style->doc = NULL;
562///         ... cleanup ...
563///         return(-1);
564///     }
565///     return(0);
566/// }
567/// ```
568///
569/// # ENGINE-WIRING
570///
571/// `xsltGatherNamespaces` (namespaces.c) builds `style->nsHash` for the
572/// upstream engine; the candidate resolves namespaces at runtime from the
573/// node tree, so the call has no candidate equivalent (documented
574/// divergence — `nsHash` is unused by the engine).
575///
576/// # SAFETY
577///
578/// - `style` must be a valid `_xsltStylesheet`, or NULL.
579/// - `doc` must be a valid parsed document, or NULL.
580#[no_mangle]
581pub unsafe extern "C" fn xsltParseStylesheetUser(
582    style: *mut _xsltStylesheet,
583    doc: *mut _xmlDoc,
584) -> c_int {
585    if style.is_null() || doc.is_null() {
586        return -1;
587    }
588
589    // Adjust the string dict (xslt.c 1.1.45).
590    if !(*doc).dict.is_null() {
591        xmlDictFree((*style).dict);
592        (*style).dict = (*doc).dict;
593        xmlDictReference((*style).dict);
594    }
595
596    // xsltGatherNamespaces(style) — no-op in the candidate engine, see
597    // module docs.
598
599    (*style).doc = doc;
600    if xsltParseStylesheetProcess(style, doc).is_null() {
601        (*style).doc = ptr::null_mut();
602        return -1;
603    }
604
605    if (*style).parent.is_null() {
606        crate::abi::exports_xslt_apply::xsltResolveStylesheetAttributeSet(style);
607    }
608
609    if (*style).errors != 0 {
610        // Detach the doc from the stylesheet; otherwise the doc would be
611        // freed by xsltFreeStylesheet(). The caller keeps ownership.
612        (*style).doc = ptr::null_mut();
613        return -1;
614    }
615
616    0
617}
618
619/// Parse an XSLT stylesheet from a document, with a parent stylesheet
620/// context (used for `xsl:import`).
621///
622/// # UPSTREAM-PARITY
623///
624/// ```c
625/// xsltStylesheetPtr
626/// xsltParseStylesheetImportedDoc(xmlDocPtr doc,
627///                                xsltStylesheetPtr parentStyle) {
628///     if (doc == NULL) return(NULL);
629///     retStyle = xsltNewStylesheetInternal(parentStyle);
630///     if (retStyle == NULL) return(NULL);
631///     if (xsltParseStylesheetUser(retStyle, doc) != 0) {
632///         xsltFreeStylesheet(retStyle);
633///         return(NULL);
634///     }
635///     return(retStyle);
636/// }
637/// ```
638///
639/// # SAFETY
640///
641/// - `doc` must be a valid parsed document, or NULL. On failure the
642///   document is detached from the stylesheet and remains owned by the
643///   caller.
644#[no_mangle]
645pub unsafe extern "C" fn xsltParseStylesheetImportedDoc(
646    doc: *mut _xmlDoc,
647    parentStyle: *mut _xsltStylesheet,
648) -> *mut _xsltStylesheet {
649    if doc.is_null() {
650        return ptr::null_mut();
651    }
652
653    let retStyle = xslt_new_stylesheet_internal(parentStyle);
654    if retStyle.is_null() {
655        return ptr::null_mut();
656    }
657
658    if xsltParseStylesheetUser(retStyle, doc) != 0 {
659        crate::xslt::stylesheet::xsltFreeStylesheet(retStyle);
660        return ptr::null_mut();
661    }
662
663    retStyle
664}
665
666// ═══════════════════════════════════════════════════════════════════════════════
667// 2. Imports & includes (imports.c)
668// ═══════════════════════════════════════════════════════════════════════════════
669
670/// `xsltFixImportedCompSteps` (imports.c): normalize the compiled steps of
671/// an imported stylesheet against the master's extra slots.
672///
673/// # ENGINE-WIRING
674///
675/// Upstream scans the imported templates hash with `xsltNormalizeCompSteps`
676/// (which re-bases step extra indices). The candidate's compiled patterns
677/// carry no step-extra state (`xsltNormalizeCompSteps` is a no-op), so
678/// only the `extrasNr` accumulation is observable.
679unsafe fn xslt_fix_imported_comp_steps(master: *mut _xsltStylesheet, style: *mut _xsltStylesheet) {
680    (*master).extrasNr += (*style).extrasNr;
681    let mut res = (*style).imports;
682    while !res.is_null() {
683        xslt_fix_imported_comp_steps(master, res);
684        res = (*res).next;
685    }
686}
687
688/// `xsltCheckCycle` (imports.c): detect import/include recursion.
689unsafe fn xslt_check_cycle(
690    style: *mut _xsltStylesheet,
691    cur: *mut _xmlNode,
692    uri: *const xmlChar,
693) -> c_int {
694    let mut depth: c_int = 0;
695    let mut ancestor = style;
696    while !ancestor.is_null() {
697        depth += 1;
698        if depth >= XSLT_MAX_NESTING {
699            report_error(style, cur, b"maximum nesting depth exceeded: ");
700            report_error(style, cur, cbytes(uri as *const u8));
701            report_error(style, cur, b"\n");
702            return -1;
703        }
704        if !(*ancestor).doc.is_null()
705            && !(*(*ancestor).doc).URL.is_null()
706            && xmlStrEqual((*(*ancestor).doc).URL, uri) != 0
707        {
708            report_error(style, cur, b"recursion detected on imported URL ");
709            report_error(style, cur, cbytes(uri as *const u8));
710            report_error(style, cur, b"\n");
711            return -1;
712        }
713
714        // Check included stylesheets.
715        let mut docptr = (*ancestor).includes;
716        while !docptr.is_null() {
717            depth += 1;
718            if depth >= XSLT_MAX_NESTING {
719                report_error(style, cur, b"maximum nesting depth exceeded: ");
720                report_error(style, cur, cbytes(uri as *const u8));
721                report_error(style, cur, b"\n");
722                return -1;
723            }
724            if !(*docptr).doc.is_null()
725                && !(*(*docptr).doc).URL.is_null()
726                && xmlStrEqual((*(*docptr).doc).URL, uri) != 0
727            {
728                report_error(style, cur, b"recursion detected on included URL ");
729                report_error(style, cur, cbytes(uri as *const u8));
730                report_error(style, cur, b"\n");
731                return -1;
732            }
733            docptr = (*docptr).includes;
734        }
735
736        ancestor = (*ancestor).parent;
737    }
738
739    0
740}
741
742/// Parse an XSLT stylesheet import element.
743///
744/// # UPSTREAM-PARITY
745///
746/// ```c
747/// int
748/// xsltParseStylesheetImport(xsltStylesheetPtr style, xmlNodePtr cur) {
749///     ... href/base/URI resolution, cycle + security checks,
750///         xsltDocDefaultLoader(...), xsltParseStylesheetImportedDoc(),
751///         res->next = style->imports; style->imports = res;
752///         xsltFixImportedCompSteps(style, res) when style->parent == NULL
753/// }
754/// ```
755///
756/// Returns 0 on success, -1 on failure.
757///
758/// # SAFETY
759///
760/// - `style` must be a valid `_xsltStylesheet`, or NULL.
761/// - `cur` must be a valid `xsl:import` element node, or NULL.
762#[no_mangle]
763pub unsafe extern "C" fn xsltParseStylesheetImport(
764    style: *mut _xsltStylesheet,
765    cur: *mut _xmlNode,
766) -> c_int {
767    let mut ret: c_int = -1;
768    let mut uriRef: *mut xmlChar = ptr::null_mut();
769    let mut base: *mut xmlChar = ptr::null_mut();
770    let mut uri: *mut xmlChar = ptr::null_mut();
771
772    if cur.is_null() || style.is_null() {
773        return ret;
774    }
775
776    uriRef = xmlGetNsProp(cur, b"href\0".as_ptr() as *const xmlChar, ptr::null());
777    if uriRef.is_null() {
778        report_error(style, cur, b"xsl:import : missing href attribute\n");
779        if !uriRef.is_null() {
780            xmlFreeImpl(uriRef as *mut c_void);
781        }
782        if !base.is_null() {
783            xmlFreeImpl(base as *mut c_void);
784        }
785        if !uri.is_null() {
786            xmlFreeImpl(uri as *mut c_void);
787        }
788        return ret;
789    }
790
791    base = xmlNodeGetBase((*style).doc, cur);
792    uri = xmlBuildURI(uriRef as *const c_char, base as *const c_char);
793    if uri.is_null() {
794        report_error(style, cur, b"xsl:import : invalid URI reference ");
795        report_error(style, cur, cbytes(uriRef as *const u8));
796        report_error(style, cur, b"\n");
797        if !uriRef.is_null() {
798            xmlFreeImpl(uriRef as *mut c_void);
799        }
800        if !base.is_null() {
801            xmlFreeImpl(base as *mut c_void);
802        }
803        if !uri.is_null() {
804            xmlFreeImpl(uri as *mut c_void);
805        }
806        return ret;
807    }
808
809    if xslt_check_cycle(style, cur, uri) < 0 {
810        if !uriRef.is_null() {
811            xmlFreeImpl(uriRef as *mut c_void);
812        }
813        if !base.is_null() {
814            xmlFreeImpl(base as *mut c_void);
815        }
816        if !uri.is_null() {
817            xmlFreeImpl(uri as *mut c_void);
818        }
819        return ret;
820    }
821
822    // Security framework check.
823    let sec = crate::xslt::security::xsltGetDefaultSecurityPrefs();
824    if !sec.is_null() {
825        let secres = xslt_check_read(sec, ptr::null_mut(), uri);
826        if secres <= 0 {
827            if secres == 0 {
828                report_error(
829                    ptr::null_mut(),
830                    ptr::null_mut(),
831                    b"xsl:import: read rights for ",
832                );
833                report_error(ptr::null_mut(), ptr::null_mut(), cbytes(uri as *const u8));
834                report_error(ptr::null_mut(), ptr::null_mut(), b" denied\n");
835            }
836            if !uriRef.is_null() {
837                xmlFreeImpl(uriRef as *mut c_void);
838            }
839            if !base.is_null() {
840                xmlFreeImpl(base as *mut c_void);
841            }
842            if !uri.is_null() {
843                xmlFreeImpl(uri as *mut c_void);
844            }
845            return ret;
846        }
847    }
848
849    let import = xslt_doc_default_loader(
850        uri,
851        (*style).dict,
852        XSLT_PARSE_OPTIONS,
853        style as *mut c_void,
854        XSLT_LOAD_STYLESHEET,
855    );
856    if import.is_null() {
857        report_error(style, cur, b"xsl:import : unable to load ");
858        report_error(style, cur, cbytes(uri as *const u8));
859        report_error(style, cur, b"\n");
860        if !uriRef.is_null() {
861            xmlFreeImpl(uriRef as *mut c_void);
862        }
863        if !base.is_null() {
864            xmlFreeImpl(base as *mut c_void);
865        }
866        if !uri.is_null() {
867            xmlFreeImpl(uri as *mut c_void);
868        }
869        return ret;
870    }
871
872    let res = xsltParseStylesheetImportedDoc(import, style);
873    if !res.is_null() {
874        (*res).next = (*style).imports;
875        (*style).imports = res;
876        if (*style).parent.is_null() {
877            xslt_fix_imported_comp_steps(style, res);
878        }
879        ret = 0;
880    } else {
881        crate::xml::tree::free_doc(import);
882    }
883
884    if !uriRef.is_null() {
885        xmlFreeImpl(uriRef as *mut c_void);
886    }
887    if !base.is_null() {
888        xmlFreeImpl(base as *mut c_void);
889    }
890    if !uri.is_null() {
891        xmlFreeImpl(uri as *mut c_void);
892    }
893
894    ret
895}
896
897/// Parse an XSLT stylesheet include element.
898///
899/// # UPSTREAM-PARITY
900///
901/// ```c
902/// int
903/// xsltParseStylesheetInclude(xsltStylesheetPtr style, xmlNodePtr cur) {
904///     ... href/base/URI resolution, cycle check,
905///         include = xsltLoadStyleDocument(style, URI);
906///         oldDoc = style->doc; style->doc = include->doc;
907///         include->includes = style->includes; style->includes = include;
908///         oldNopreproc = style->nopreproc;
909///         style->nopreproc = include->preproc;
910///         result = xsltParseStylesheetProcess(style, include->doc);
911///         style->nopreproc = oldNopreproc;
912///         include->preproc = 1;
913///         style->includes = include->includes;
914///         style->doc = oldDoc;
915///         if (result == NULL) { ret = -1; goto error; }
916///         ret = 0;
917/// }
918/// ```
919///
920/// Returns 0 on success, -1 on failure.
921///
922/// # SAFETY
923///
924/// - `style` must be a valid `_xsltStylesheet`, or NULL.
925/// - `cur` must be a valid `xsl:include` element node, or NULL.
926#[no_mangle]
927pub unsafe extern "C" fn xsltParseStylesheetInclude(
928    style: *mut _xsltStylesheet,
929    cur: *mut _xmlNode,
930) -> c_int {
931    let mut ret: c_int = -1;
932    let mut uriRef: *mut xmlChar = ptr::null_mut();
933    let mut base: *mut xmlChar = ptr::null_mut();
934    let mut uri: *mut xmlChar = ptr::null_mut();
935
936    if cur.is_null() || style.is_null() {
937        return ret;
938    }
939
940    uriRef = xmlGetNsProp(cur, b"href\0".as_ptr() as *const xmlChar, ptr::null());
941    if uriRef.is_null() {
942        report_error(style, cur, b"xsl:include : missing href attribute\n");
943        if !uriRef.is_null() {
944            xmlFreeImpl(uriRef as *mut c_void);
945        }
946        if !base.is_null() {
947            xmlFreeImpl(base as *mut c_void);
948        }
949        if !uri.is_null() {
950            xmlFreeImpl(uri as *mut c_void);
951        }
952        return ret;
953    }
954
955    base = xmlNodeGetBase((*style).doc, cur);
956    uri = xmlBuildURI(uriRef as *const c_char, base as *const c_char);
957    if uri.is_null() {
958        report_error(style, cur, b"xsl:include : invalid URI reference ");
959        report_error(style, cur, cbytes(uriRef as *const u8));
960        report_error(style, cur, b"\n");
961        if !uriRef.is_null() {
962            xmlFreeImpl(uriRef as *mut c_void);
963        }
964        if !base.is_null() {
965            xmlFreeImpl(base as *mut c_void);
966        }
967        if !uri.is_null() {
968            xmlFreeImpl(uri as *mut c_void);
969        }
970        return ret;
971    }
972
973    if xslt_check_cycle(style, cur, uri) < 0 {
974        if !uriRef.is_null() {
975            xmlFreeImpl(uriRef as *mut c_void);
976        }
977        if !base.is_null() {
978            xmlFreeImpl(base as *mut c_void);
979        }
980        if !uri.is_null() {
981            xmlFreeImpl(uri as *mut c_void);
982        }
983        return ret;
984    }
985
986    let include = xsltLoadStyleDocument(style, uri);
987    if include.is_null() {
988        report_error(style, cur, b"xsl:include : unable to load ");
989        report_error(style, cur, cbytes(uri as *const u8));
990        report_error(style, cur, b"\n");
991        if !uriRef.is_null() {
992            xmlFreeImpl(uriRef as *mut c_void);
993        }
994        if !base.is_null() {
995            xmlFreeImpl(base as *mut c_void);
996        }
997        if !uri.is_null() {
998            xmlFreeImpl(uri as *mut c_void);
999        }
1000        return ret;
1001    }
1002
1003    let oldDoc = (*style).doc;
1004    (*style).doc = (*include).doc;
1005    // Chain to the stylesheet for recursion checking.
1006    (*include).includes = (*style).includes;
1007    (*style).includes = include;
1008    let oldNopreproc = (*style).nopreproc;
1009    (*style).nopreproc = (*include).preproc;
1010    // ENGINE-WIRING: upstream skips the whole-tree preprocessing when the
1011    // include was already preprocessed (`include->preproc`); the candidate
1012    // compiler's preprocessing is idempotent (blank-stripping and text
1013    // merging), so re-running it is safe. The `nopreproc` flag is restored
1014    // exactly like upstream.
1015    let result = xsltParseStylesheetProcess(style, (*include).doc);
1016    (*style).nopreproc = oldNopreproc;
1017    (*include).preproc = 1;
1018    (*style).includes = (*include).includes;
1019    (*style).doc = oldDoc;
1020    if result.is_null() {
1021        ret = -1;
1022        if !uriRef.is_null() {
1023            xmlFreeImpl(uriRef as *mut c_void);
1024        }
1025        if !base.is_null() {
1026            xmlFreeImpl(base as *mut c_void);
1027        }
1028        if !uri.is_null() {
1029            xmlFreeImpl(uri as *mut c_void);
1030        }
1031        return ret;
1032    }
1033    ret = 0;
1034
1035    if !uriRef.is_null() {
1036        xmlFreeImpl(uriRef as *mut c_void);
1037    }
1038    if !base.is_null() {
1039        xmlFreeImpl(base as *mut c_void);
1040    }
1041    if !uri.is_null() {
1042        xmlFreeImpl(uri as *mut c_void);
1043    }
1044    return ret;
1045}
1046
1047// ═══════════════════════════════════════════════════════════════════════════════
1048// 3. Top-level constructs (xslt.c, attributes.c, variables.c)
1049// ═══════════════════════════════════════════════════════════════════════════════
1050
1051/// `xsltParseContentError` (xslt.c): report a misplaced child node.
1052unsafe fn xslt_parse_content_error(style: *mut _xsltStylesheet, node: *mut _xmlNode) {
1053    if style.is_null() || node.is_null() {
1054        return;
1055    }
1056    if is_xslt_elem(node) {
1057        report_error(
1058            style,
1059            node,
1060            b"The XSLT-element is not allowed at this position.\n",
1061        );
1062    } else {
1063        report_error(
1064            style,
1065            node,
1066            b"The element is not allowed at this position.\n",
1067        );
1068    }
1069    (*style).errors += 1;
1070}
1071
1072/// Parse an XSLT stylesheet output element and record the output settings.
1073///
1074/// # UPSTREAM-PARITY
1075///
1076/// ```c
1077/// void
1078/// xsltParseStylesheetOutput(xsltStylesheetPtr style, xmlNodePtr cur);
1079/// ```
1080///
1081/// Ported from xslt.c 1.1.45: version/encoding/method (with QName
1082/// resolution via `xsltGetQNameURI`), doctype-system/public, standalone,
1083/// indent, omit-xml-declaration, cdata-section-elements (a
1084/// `{name, ns-URI}` hash holding the sentinel "cdata"), media-type, and
1085/// the content-error check for children. Invalid enum values bump
1086/// `style->errors`; an invalid method bumps `style->warnings`.
1087///
1088/// # SAFETY
1089///
1090/// - `style` must be a valid `_xsltStylesheet`, or NULL.
1091/// - `cur` must be a valid `xsl:output` element node, or NULL.
1092#[no_mangle]
1093pub unsafe extern "C" fn xsltParseStylesheetOutput(
1094    style: *mut _xsltStylesheet,
1095    cur: *mut _xmlNode,
1096) {
1097    if cur.is_null() || style.is_null() || (*cur).type_ != XML_ELEMENT_NODE as c_int {
1098        return;
1099    }
1100
1101    // version
1102    let mut prop = xmlGetNsProp(cur, b"version\0".as_ptr() as *const xmlChar, ptr::null());
1103    if !prop.is_null() {
1104        if !(*style).version.is_null() {
1105            xmlFreeImpl((*style).version as *mut c_void);
1106        }
1107        (*style).version = prop;
1108        prop = ptr::null_mut();
1109    }
1110
1111    // encoding
1112    prop = xmlGetNsProp(cur, b"encoding\0".as_ptr() as *const xmlChar, ptr::null());
1113    if !prop.is_null() {
1114        if !(*style).encoding.is_null() {
1115            xmlFreeImpl((*style).encoding as *mut c_void);
1116        }
1117        (*style).encoding = prop;
1118        prop = ptr::null_mut();
1119    }
1120
1121    // method (relaxed to support xt:document)
1122    prop = xmlGetNsProp(cur, b"method\0".as_ptr() as *const xmlChar, ptr::null());
1123    if !prop.is_null() {
1124        if !(*style).method.is_null() {
1125            xmlFreeImpl((*style).method as *mut c_void);
1126        }
1127        (*style).method = ptr::null_mut();
1128        if !(*style).methodURI.is_null() {
1129            xmlFreeImpl((*style).methodURI as *mut c_void);
1130        }
1131        (*style).methodURI = ptr::null_mut();
1132
1133        let mut method = prop;
1134        let uri = crate::abi::exports_xslt_avt::xsltGetQNameURI(cur, &mut method);
1135        if method.is_null() {
1136            if !style.is_null() {
1137                (*style).errors += 1;
1138            }
1139        } else if uri.is_null() {
1140            if xmlStrEqual(method, b"xml\0".as_ptr() as *const xmlChar) != 0
1141                || xmlStrEqual(method, b"html\0".as_ptr() as *const xmlChar) != 0
1142                || xmlStrEqual(method, b"text\0".as_ptr() as *const xmlChar) != 0
1143            {
1144                (*style).method = method;
1145            } else {
1146                report_error(style, cur, b"invalid value for method: ");
1147                report_error(style, cur, cbytes(method as *const u8));
1148                report_error(style, cur, b"\n");
1149                if !style.is_null() {
1150                    (*style).warnings += 1;
1151                }
1152                xmlFreeImpl(method as *mut c_void);
1153            }
1154        } else {
1155            (*style).method = method;
1156            (*style).methodURI = xmlStrdup(uri);
1157        }
1158        prop = ptr::null_mut();
1159    }
1160
1161    // doctype-system
1162    prop = xmlGetNsProp(
1163        cur,
1164        b"doctype-system\0".as_ptr() as *const xmlChar,
1165        ptr::null(),
1166    );
1167    if !prop.is_null() {
1168        if !(*style).doctypeSystem.is_null() {
1169            xmlFreeImpl((*style).doctypeSystem as *mut c_void);
1170        }
1171        (*style).doctypeSystem = prop;
1172        prop = ptr::null_mut();
1173    }
1174
1175    // doctype-public
1176    prop = xmlGetNsProp(
1177        cur,
1178        b"doctype-public\0".as_ptr() as *const xmlChar,
1179        ptr::null(),
1180    );
1181    if !prop.is_null() {
1182        if !(*style).doctypePublic.is_null() {
1183            xmlFreeImpl((*style).doctypePublic as *mut c_void);
1184        }
1185        (*style).doctypePublic = prop;
1186        prop = ptr::null_mut();
1187    }
1188
1189    // standalone
1190    prop = xmlGetNsProp(cur, b"standalone\0".as_ptr() as *const xmlChar, ptr::null());
1191    if !prop.is_null() {
1192        if xmlStrEqual(prop, b"yes\0".as_ptr() as *const xmlChar) != 0 {
1193            (*style).standalone = 1;
1194        } else if xmlStrEqual(prop, b"no\0".as_ptr() as *const xmlChar) != 0 {
1195            (*style).standalone = 0;
1196        } else {
1197            report_error(style, cur, b"invalid value for standalone\n");
1198            (*style).errors += 1;
1199        }
1200        xmlFreeImpl(prop as *mut c_void);
1201    }
1202
1203    // indent
1204    prop = xmlGetNsProp(cur, b"indent\0".as_ptr() as *const xmlChar, ptr::null());
1205    if !prop.is_null() {
1206        if xmlStrEqual(prop, b"yes\0".as_ptr() as *const xmlChar) != 0 {
1207            (*style).indent = 1;
1208        } else if xmlStrEqual(prop, b"no\0".as_ptr() as *const xmlChar) != 0 {
1209            (*style).indent = 0;
1210        } else {
1211            report_error(style, cur, b"invalid value for indent\n");
1212            (*style).errors += 1;
1213        }
1214        xmlFreeImpl(prop as *mut c_void);
1215    }
1216
1217    // omit-xml-declaration
1218    prop = xmlGetNsProp(
1219        cur,
1220        b"omit-xml-declaration\0".as_ptr() as *const xmlChar,
1221        ptr::null(),
1222    );
1223    if !prop.is_null() {
1224        if xmlStrEqual(prop, b"yes\0".as_ptr() as *const xmlChar) != 0 {
1225            (*style).omitXmlDeclaration = 1;
1226        } else if xmlStrEqual(prop, b"no\0".as_ptr() as *const xmlChar) != 0 {
1227            (*style).omitXmlDeclaration = 0;
1228        } else {
1229            report_error(style, cur, b"invalid value for omit-xml-declaration\n");
1230            (*style).errors += 1;
1231        }
1232        xmlFreeImpl(prop as *mut c_void);
1233    }
1234
1235    // cdata-section-elements
1236    let elements = xmlGetNsProp(
1237        cur,
1238        b"cdata-section-elements\0".as_ptr() as *const xmlChar,
1239        ptr::null(),
1240    );
1241    if !elements.is_null() {
1242        if (*style).cdataSection.is_null() {
1243            (*style).cdataSection = crate::xml::hash::hash_create(10) as *mut c_void;
1244        }
1245        if (*style).cdataSection.is_null() {
1246            xmlFreeImpl(elements as *mut c_void);
1247            return;
1248        }
1249
1250        let mut element: *mut xmlChar = elements;
1251        while *element != 0 {
1252            while matches!(*element, b' ' | b'\t' | b'\n' | b'\r') {
1253                element = element.add(1);
1254            }
1255            if *element == 0 {
1256                break;
1257            }
1258            let mut end = element;
1259            while *end != 0 && !matches!(*end, b' ' | b'\t' | b'\n' | b'\r') {
1260                end = end.add(1);
1261            }
1262            let len = end.offset_from(element) as usize;
1263            let token = xmlMallocImpl(len + 1) as *mut xmlChar;
1264            if !token.is_null() {
1265                core::ptr::copy_nonoverlapping(element, token, len);
1266                *token.add(len) = 0;
1267                if xmlValidateQName(token, 0) != 0 {
1268                    report_error(
1269                        style,
1270                        cur,
1271                        b"Attribute 'cdata-section-elements': The value is not a valid QName.\n",
1272                    );
1273                    xmlFreeImpl(token as *mut c_void);
1274                    (*style).errors += 1;
1275                } else {
1276                    let mut qname = token;
1277                    let quri = crate::abi::exports_xslt_avt::xsltGetQNameURI(cur, &mut qname);
1278                    if qname.is_null() {
1279                        report_error(
1280                            style,
1281                            cur,
1282                            b"Attribute 'cdata-section-elements': Not a valid QName.\n",
1283                        );
1284                        (*style).errors += 1;
1285                    } else {
1286                        let mut uri = quri;
1287                        // XSLT-1.0: QNames without a prefix use the default
1288                        // namespace in effect on xsl:output (bug #339570).
1289                        if uri.is_null() {
1290                            let ns = xmlSearchNs((*style).doc, cur, ptr::null());
1291                            if !ns.is_null() {
1292                                uri = (*ns).href;
1293                            }
1294                        }
1295                        crate::xml::hash::hash_add_entry2(
1296                            (*style).cdataSection as *mut crate::xml::hash::HashTable,
1297                            qname,
1298                            uri,
1299                            b"cdata\0".as_ptr() as *const c_void as *mut c_void,
1300                        );
1301                        xmlFreeImpl(qname as *mut c_void);
1302                    }
1303                }
1304            }
1305            element = end;
1306        }
1307        xmlFreeImpl(elements as *mut c_void);
1308    }
1309
1310    // media-type
1311    prop = xmlGetNsProp(cur, b"media-type\0".as_ptr() as *const xmlChar, ptr::null());
1312    if !prop.is_null() {
1313        if !(*style).mediaType.is_null() {
1314            xmlFreeImpl((*style).mediaType as *mut c_void);
1315        }
1316        (*style).mediaType = prop;
1317        prop = ptr::null_mut();
1318    }
1319
1320    // Content of xsl:output must be empty (upstream checks the first
1321    // child only).
1322    if !(*cur).children.is_null() {
1323        xslt_parse_content_error(style, (*cur).children);
1324    }
1325}
1326
1327/// Parse an XSLT stylesheet attribute-set element.
1328///
1329/// # UPSTREAM-PARITY
1330///
1331/// ```c
1332/// void
1333/// xsltParseStylesheetAttributeSet(xsltStylesheetPtr style, xmlNodePtr cur);
1334/// ```
1335///
1336/// # ENGINE-WIRING
1337///
1338/// Wired to `crate::xslt::attributes::xsltCompileAttrSet`, which records
1339/// the set (name/instruction/stylesheet) on `style->attributeSets`. The
1340/// upstream QName validation and `use-attribute-sets` processing are
1341/// subsumed: the engine resolves referenced sets by name at apply time
1342/// (`xsltApplyAttrSets`). The QName check is kept for parity.
1343///
1344/// # SAFETY
1345///
1346/// - `style` must be a valid `_xsltStylesheet`, or NULL.
1347/// - `cur` must be a valid `xsl:attribute-set` element node, or NULL.
1348#[no_mangle]
1349pub unsafe extern "C" fn xsltParseStylesheetAttributeSet(
1350    style: *mut _xsltStylesheet,
1351    cur: *mut _xmlNode,
1352) {
1353    if cur.is_null() || style.is_null() || (*cur).type_ != XML_ELEMENT_NODE as c_int {
1354        return;
1355    }
1356
1357    let value = xmlGetNsProp(cur, b"name\0".as_ptr() as *const xmlChar, ptr::null());
1358    if value.is_null() || *value == 0 {
1359        if !value.is_null() {
1360            xmlFreeImpl(value as *mut c_void);
1361        }
1362        return;
1363    }
1364    if xmlValidateQName(value, 0) != 0 {
1365        report_error(
1366            style,
1367            cur,
1368            b"xsl:attribute-set : The name is not a valid QName.\n",
1369        );
1370        (*style).errors += 1;
1371        xmlFreeImpl(value as *mut c_void);
1372        return;
1373    }
1374    xmlFreeImpl(value as *mut c_void);
1375
1376    crate::xslt::attributes::xsltCompileAttrSet(style, cur);
1377}
1378
1379/// Parse a global XSLT `variable` declaration at compilation time and
1380/// register it.
1381///
1382/// # UPSTREAM-PARITY
1383///
1384/// ```c
1385/// void
1386/// xsltParseGlobalVariable(xsltStylesheetPtr style, xmlNodePtr cur);
1387/// ```
1388///
1389/// # ENGINE-WIRING
1390///
1391/// Wired to the engine's `crate::xslt::compiler::compile_variable`
1392/// (is_param = 0), which allocates the `_xsltStackElem`, copies
1393/// name/select, records the content tree and prepends it to
1394/// `style->variables`. The upstream "missing name" and "redefinition of
1395/// global variable" diagnostics are reproduced here (the redefinition
1396/// check compares the local name only — the candidate stores no nameURI
1397/// for globals, documented divergence).
1398///
1399/// # SAFETY
1400///
1401/// - `style` must be a valid `_xsltStylesheet`, or NULL.
1402/// - `cur` must be a valid `xsl:variable` element node, or NULL.
1403#[no_mangle]
1404pub unsafe extern "C" fn xsltParseGlobalVariable(style: *mut _xsltStylesheet, cur: *mut _xmlNode) {
1405    if cur.is_null() || style.is_null() || (*cur).type_ != XML_ELEMENT_NODE as c_int {
1406        return;
1407    }
1408
1409    let name = xmlGetNsProp(cur, b"name\0".as_ptr() as *const xmlChar, ptr::null());
1410    if name.is_null() {
1411        report_error(style, cur, b"xsl:variable : missing name attribute\n");
1412        return;
1413    }
1414
1415    // Upstream reports a redefinition error for duplicate global
1416    // variables (not params).
1417    let mut tmp = (*style).variables;
1418    while !tmp.is_null() {
1419        if ((*tmp).flags & XSLT_VAR_PARAM) == 0
1420            && !(*tmp).name.is_null()
1421            && xmlStrEqual((*tmp).name, name) != 0
1422        {
1423            report_error(style, cur, b"redefinition of global variable ");
1424            report_error(style, cur, cbytes(name as *const u8));
1425            report_error(style, cur, b"\n");
1426            (*style).errors += 1;
1427            break;
1428        }
1429        tmp = (*tmp).next;
1430    }
1431    xmlFreeImpl(name as *mut c_void);
1432
1433    // Parse the content (a sequence constructor).
1434    if !(*cur).children.is_null() {
1435        xsltParseTemplateContent(style, cur);
1436    }
1437
1438    crate::xslt::compiler::compile_variable(style, cur, 0, 0);
1439}
1440
1441/// Parse a global XSLT `param` declaration at compilation time and
1442/// register it.
1443///
1444/// # UPSTREAM-PARITY
1445///
1446/// ```c
1447/// void
1448/// xsltParseGlobalParam(xsltStylesheetPtr style, xmlNodePtr cur);
1449/// ```
1450///
1451/// # ENGINE-WIRING
1452///
1453/// Same as `xsltParseGlobalVariable` with is_param = 1 (the engine marks
1454/// the stack element with the `XSLT_VAR_PARAM` flag).
1455///
1456/// # SAFETY
1457///
1458/// - `style` must be a valid `_xsltStylesheet`, or NULL.
1459/// - `cur` must be a valid `xsl:param` element node, or NULL.
1460#[no_mangle]
1461pub unsafe extern "C" fn xsltParseGlobalParam(style: *mut _xsltStylesheet, cur: *mut _xmlNode) {
1462    if cur.is_null() || style.is_null() || (*cur).type_ != XML_ELEMENT_NODE as c_int {
1463        return;
1464    }
1465
1466    let name = xmlGetNsProp(cur, b"name\0".as_ptr() as *const xmlChar, ptr::null());
1467    if name.is_null() {
1468        report_error(style, cur, b"xsl:param : missing name attribute\n");
1469        return;
1470    }
1471    xmlFreeImpl(name as *mut c_void);
1472
1473    // Parse the content (a sequence constructor).
1474    if !(*cur).children.is_null() {
1475        xsltParseTemplateContent(style, cur);
1476    }
1477
1478    crate::xslt::compiler::compile_variable(style, cur, 0, 1);
1479}
1480
1481// ═══════════════════════════════════════════════════════════════════════════════
1482// 4. Template content & attribute compilation (xslt.c, attrvt.c)
1483// ═══════════════════════════════════════════════════════════════════════════════
1484
1485/// Parse a template content-model: precompute each XSLT instruction and
1486/// the AVTs of literal result elements.
1487///
1488/// # UPSTREAM-PARITY
1489///
1490/// ```c
1491/// void
1492/// xsltParseTemplateContent(xsltStylesheetPtr style, xmlNodePtr templ);
1493/// ```
1494///
1495/// Ported from xslt.c 1.1.45 (old behaviour): walk the subtree, run
1496/// `xsltStylePreCompute` on XSLT and extension elements, `xsltCompileAttr`
1497/// on literal-result-element attributes, and remove misplaced `xsl:param`
1498/// elements (with a warning).
1499///
1500/// # ENGINE-WIRING
1501///
1502/// Upstream *replaces* `xsl:text` with its children during this pass and
1503/// deletes the instruction node. The candidate engine evaluates `xsl:text`
1504/// directly at runtime (`xsltProcessInstruction` → `process_text`), so the
1505/// unwrap/delete is intentionally skipped — the tree stays intact
1506/// (documented divergence; observable output is identical).
1507///
1508/// # SAFETY
1509///
1510/// - `style` must be a valid `_xsltStylesheet`, or NULL.
1511/// - `templ` must be a valid node whose children form the content, or
1512///   NULL.
1513#[no_mangle]
1514pub unsafe extern "C" fn xsltParseTemplateContent(
1515    style: *mut _xsltStylesheet,
1516    templ: *mut _xmlNode,
1517) {
1518    if style.is_null() || templ.is_null() || (*templ).type_ == XML_NAMESPACE_DECL as c_int {
1519        return;
1520    }
1521
1522    let mut cur = (*templ).children;
1523    while !cur.is_null() {
1524        if !(*style).principal.is_null() {
1525            (*(*style).principal).opCount += 1;
1526        }
1527
1528        if is_xslt_elem(cur) {
1529            xsltStylePreCompute(style, cur);
1530            // xsl:text is evaluated at runtime by the engine; upstream's
1531            // unwrap + node deletion is not performed (see module docs).
1532        } else if !(*cur).ns.is_null() && !ext_ns_registered((*(*cur).ns).href) {
1533            // Not an XSLT element and not a registered extension element:
1534            // falls through to the literal-result-element branch below.
1535        } else if !(*cur).ns.is_null() && ext_ns_registered((*(*cur).ns).href) {
1536            // Extension element: compile it too.
1537            xsltStylePreCompute(style, cur);
1538        } else if (*cur).type_ == XML_ELEMENT_NODE as c_int {
1539            // A literal result element: precompile the AVTs of its
1540            // attributes.
1541            if (*cur).ns.is_null() && !(*style).defaultAlias.is_null() {
1542                (*cur).ns = xmlSearchNsByHref((*cur).doc, cur, (*style).defaultAlias);
1543            }
1544            if !(*cur).properties.is_null() {
1545                let mut attr = (*cur).properties;
1546                while !attr.is_null() {
1547                    xsltCompileAttr(style, attr);
1548                    attr = (*attr).next;
1549                }
1550            }
1551        }
1552
1553        // Descend into children, else next sibling, else pop up to
1554        // `templ`.
1555        if !(*cur).children.is_null() {
1556            if (*(*cur).children).type_ != XML_ENTITY_DECL as c_int {
1557                cur = (*cur).children;
1558                continue;
1559            }
1560        }
1561        if !(*cur).next.is_null() {
1562            cur = (*cur).next;
1563            continue;
1564        }
1565        loop {
1566            cur = (*cur).parent;
1567            if cur.is_null() {
1568                break;
1569            }
1570            if cur == templ {
1571                cur = ptr::null_mut();
1572                break;
1573            }
1574            if !(*cur).next.is_null() {
1575                cur = (*cur).next;
1576                break;
1577            }
1578        }
1579    }
1580
1581    // Skip the first params.
1582    let mut cur = (*templ).children;
1583    while !cur.is_null() {
1584        if is_xslt_elem(cur) && !is_xslt_name(cur, b"param") {
1585            break;
1586        }
1587        cur = (*cur).next;
1588    }
1589
1590    // Browse the remainder of the template, removing misplaced params.
1591    while !cur.is_null() {
1592        if is_xslt_elem(cur) && is_xslt_name(cur, b"param") {
1593            let param = cur;
1594            report_error(
1595                style,
1596                cur,
1597                b"xsltParseTemplateContent: ignoring misplaced param element\n",
1598            );
1599            if !style.is_null() {
1600                (*style).warnings += 1;
1601            }
1602            cur = (*cur).next;
1603            crate::xml::tree::unlink_node(param);
1604            crate::xml::tree::free_node(param);
1605        } else {
1606            break;
1607        }
1608    }
1609}
1610
1611/// Whether a namespace URI is registered as an extension namespace (used
1612/// to distinguish extension elements from literal result elements).
1613unsafe fn ext_ns_registered(uri: *const xmlChar) -> bool {
1614    if uri.is_null() {
1615        return false;
1616    }
1617    let mut cur = XSLT_ELEMENTS_REGISTRY;
1618    while !cur.is_null() {
1619        if !(*cur).URI.is_null() && xmlStrEqual((*cur).URI, uri) != 0 {
1620            return true;
1621        }
1622        cur = (*cur).next;
1623    }
1624    false
1625}
1626
1627/// Precompile an attribute in a stylesheet: check whether it is an
1628/// attribute value template and validate its structure.
1629///
1630/// # UPSTREAM-PARITY
1631///
1632/// ```c
1633/// void
1634/// xsltCompileAttr(xsltStylesheetPtr style, xmlAttrPtr attr);
1635/// ```
1636///
1637/// # ENGINE-WIRING
1638///
1639/// Upstream parses the AVT into a segment list (`xsltAttrVT`) and stores
1640/// it in `attr->psvi` / `style->attVTs`. The candidate engine evaluates
1641/// AVTs lazily at transform time from the raw attribute string
1642/// (`crate::xslt::transform::eval_avt`), so no AVT object is allocated;
1643/// the compile-time *diagnostics* are kept for parity: a multi-node or
1644/// non-text attribute content, and unmatched `{`/`}` (an unmatched `}` is
1645/// reported without bumping the error counter, exactly like upstream).
1646///
1647/// # SAFETY
1648///
1649/// - `style` must be a valid `_xsltStylesheet`, or NULL.
1650/// - `attr` must be a valid attribute of the stylesheet tree, or NULL.
1651#[no_mangle]
1652pub unsafe extern "C" fn xsltCompileAttr(style: *mut _xsltStylesheet, attr: *mut _xmlAttr) {
1653    if style.is_null() || attr.is_null() || (*attr).children.is_null() {
1654        return;
1655    }
1656    if (*(*attr).children).type_ != XML_TEXT_NODE as c_int || !(*(*attr).children).next.is_null() {
1657        report_error(
1658            style,
1659            (*attr).parent,
1660            b"Attribute ': The content is expected to be a single text node when compiling an AVT.\n",
1661        );
1662        (*style).errors += 1;
1663        return;
1664    }
1665
1666    let str_ = (*(*attr).children).content;
1667    if xmlStrchr(str_, b'{' as xmlChar).is_null() && xmlStrchr(str_, b'}' as xmlChar).is_null() {
1668        return;
1669    }
1670    if !(*attr).psvi.is_null() {
1671        // Already compiled.
1672        return;
1673    }
1674
1675    // Validate the AVT structure (no object is built — the engine
1676    // evaluates the raw string lazily).
1677    let mut cur = str_;
1678    while *cur != 0 {
1679        if *cur == b'{' {
1680            if !cur.add(1).is_null() && *cur.add(1) == b'{' {
1681                // Escaped '{'.
1682                cur = cur.add(2);
1683                continue;
1684            }
1685            if !cur.add(1).is_null() && *cur.add(1) == b'}' {
1686                // Empty AVT.
1687                cur = cur.add(2);
1688                continue;
1689            }
1690            // Scan to the closing '}', honouring quoted literals
1691            // (bug539741).
1692            let mut p = cur.add(1);
1693            while *p != 0 && *p != b'}' {
1694                if *p == b'\'' || *p == b'"' {
1695                    let delim = *p;
1696                    p = p.add(1);
1697                    while *p != 0 && *p != delim {
1698                        p = p.add(1);
1699                    }
1700                    if *p != 0 {
1701                        p = p.add(1);
1702                    }
1703                } else {
1704                    p = p.add(1);
1705                }
1706            }
1707            if *p == 0 {
1708                report_error(
1709                    style,
1710                    (*attr).parent,
1711                    b"Attribute ': The AVT has an unmatched '{'.\n",
1712                );
1713                (*style).errors += 1;
1714                return;
1715            }
1716            cur = p.add(1);
1717        } else if *cur == b'}' {
1718            if !cur.add(1).is_null() && *cur.add(1) == b'}' {
1719                // Escaped '}'.
1720                cur = cur.add(2);
1721                continue;
1722            }
1723            report_error(
1724                style,
1725                (*attr).parent,
1726                b"Attribute ': The AVT has an unmatched '}'.\n",
1727            );
1728            return;
1729        } else {
1730            cur = cur.add(1);
1731        }
1732    }
1733}
1734
1735// ═══════════════════════════════════════════════════════════════════════════════
1736// 5. Precomputed instructions (preproc.c, extensions.c)
1737// ═══════════════════════════════════════════════════════════════════════════════
1738
1739/// Free a (non-extension) style precomp: upstream also releases the
1740/// compiled XPath expression, number patterns and ns-list; the candidate
1741/// engine compiles lazily, so none of those exist and only the struct is
1742/// freed.
1743unsafe fn xslt_free_style_pre_comp(comp: *mut c_void) {
1744    if comp.is_null() {
1745        return;
1746    }
1747    xmlFreeImpl(comp);
1748}
1749
1750/// `xsltFreeElemPreComp` (extensions.c).
1751unsafe extern "C" fn xslt_free_elem_pre_comp(comp: *mut c_void) {
1752    xmlFreeImpl(comp);
1753}
1754
1755/// `xsltNewStylePreComp` (preproc.c) for the non-refactored engine: build
1756/// an old-style precomp of the requested type and chain it onto
1757/// `style->preComps`.
1758///
1759/// # ENGINE-WIRING
1760///
1761/// The upstream per-type transform-function assignment (xsltCopy, xsltIf,
1762/// …) is omitted: the candidate dispatches instructions by node name at
1763/// runtime, so `func` is only meaningful to external readers of the
1764/// structure.
1765unsafe fn xslt_new_style_pre_comp(
1766    style: *mut _xsltStylesheet,
1767    type_: c_int,
1768) -> *mut _xsltStylePreComp {
1769    if style.is_null() {
1770        return ptr::null_mut();
1771    }
1772    let cur = xmlMallocImpl(core::mem::size_of::<_xsltStylePreComp>()) as *mut _xsltStylePreComp;
1773    if cur.is_null() {
1774        report_error(
1775            style,
1776            ptr::null_mut(),
1777            b"xsltNewStylePreComp : malloc failed\n",
1778        );
1779        (*style).errors += 1;
1780        return ptr::null_mut();
1781    }
1782    ptr::write_bytes(cur as *mut u8, 0, core::mem::size_of::<_xsltStylePreComp>());
1783
1784    (*cur).base.type_ = type_;
1785    (*cur).base.next = (*style).preComps as *mut _xsltElemPreComp;
1786    (*style).preComps = cur as *mut c_void;
1787
1788    cur
1789}
1790
1791/// Preprocess an XSLT-1.1 `document` (and the saxon/xalan/xt/exslt
1792/// document-like extension) element.
1793///
1794/// # UPSTREAM-PARITY
1795///
1796/// ```c
1797/// xsltElemPreCompPtr
1798/// xsltDocumentComp(xsltStylesheetPtr style, xmlNodePtr inst,
1799///                  xsltTransformFunction function ATTRIBUTE_UNUSED);
1800/// ```
1801///
1802/// Allocates an old-style precomp of type `XSLT_FUNC_DOCUMENT`, evaluates
1803/// the static `file`/`href` attribute template (`has_filename`), and marks
1804/// `ver11` when the element is `xsl:document` in the XSLT namespace.
1805///
1806/// # SAFETY
1807///
1808/// - `style` must be a valid `_xsltStylesheet`.
1809/// - `inst` must be a valid instruction element node.
1810#[no_mangle]
1811pub unsafe extern "C" fn xsltDocumentComp(
1812    style: *mut _xsltStylesheet,
1813    inst: *mut _xmlNode,
1814    _function: Option<xsltTransformFunction>,
1815) -> *mut _xsltElemPreComp {
1816    if style.is_null() || inst.is_null() || (*inst).type_ != XML_ELEMENT_NODE as c_int {
1817        return ptr::null_mut();
1818    }
1819
1820    let comp = xslt_new_style_pre_comp(style, XSLT_FUNC_DOCUMENT);
1821    if comp.is_null() {
1822        return ptr::null_mut();
1823    }
1824    (*comp).base.inst = inst;
1825    (*comp).ver11 = 0;
1826    let mut filename: *const xmlChar = ptr::null();
1827
1828    if is_xslt_name(inst, b"output") {
1829        // saxon:output — @file is an AVT.
1830        filename = crate::abi::exports_xslt_avt::xsltEvalStaticAttrValueTemplate(
1831            style,
1832            inst,
1833            b"file\0".as_ptr() as *const xmlChar,
1834            ptr::null(),
1835            &mut (*comp).has_filename,
1836        );
1837    } else if is_xslt_name(inst, b"write") {
1838        // xalan:write — the filename is interpreted at run time.
1839    } else if is_xslt_name(inst, b"document") {
1840        if !(*inst).ns.is_null() {
1841            if xmlStrEqual(
1842                (*(*inst).ns).href,
1843                XSLT_NAMESPACE.as_ptr() as *const xmlChar,
1844            ) != 0
1845            {
1846                // xsl:document from the abandoned XSLT 1.1 draft.
1847                (*comp).ver11 = 1;
1848            }
1849            // exslt:document / xt:document need no extra marking.
1850        }
1851        filename = crate::abi::exports_xslt_avt::xsltEvalStaticAttrValueTemplate(
1852            style,
1853            inst,
1854            b"href\0".as_ptr() as *const xmlChar,
1855            ptr::null(),
1856            &mut (*comp).has_filename,
1857        );
1858    }
1859    if (*comp).has_filename != 0 {
1860        (*comp).filename = filename;
1861    }
1862
1863    &mut (*comp).base as *mut _xsltElemPreComp
1864}
1865
1866/// `xsltInitElemPreComp` (extensions.c): initialize an existing precomp
1867/// and chain it onto the stylesheet's precomp list.
1868///
1869/// This helper is intentionally not exported (the ext-family ABI exports
1870/// own the public symbol); it is used by the compile family to initialize
1871/// extension-element precomps.
1872///
1873/// # SAFETY
1874///
1875/// - All pointers must be valid.
1876unsafe fn xslt_init_elem_pre_comp(
1877    comp: *mut _xsltElemPreComp,
1878    style: *mut _xsltStylesheet,
1879    inst: *mut _xmlNode,
1880    function: Option<xsltTransformFunction>,
1881    free_func: Option<xsltElemPreCompDeallocator>,
1882) {
1883    (*comp).type_ = XSLT_FUNC_EXTENSION;
1884    (*comp).func = function;
1885    (*comp).inst = inst;
1886    (*comp).free = free_func;
1887
1888    (*comp).next = (*style).preComps as *mut _xsltElemPreComp;
1889    (*style).preComps = comp as *mut c_void;
1890}
1891
1892/// `xsltNewElemPreComp` (extensions.c): allocate and initialize an
1893/// `_xsltElemPreComp`.
1894///
1895/// # SAFETY
1896///
1897/// - `style` must be a valid `_xsltStylesheet`.
1898/// - `inst` must be a valid element node.
1899unsafe fn xslt_new_elem_pre_comp(
1900    style: *mut _xsltStylesheet,
1901    inst: *mut _xmlNode,
1902    function: Option<xsltTransformFunction>,
1903) -> *mut _xsltElemPreComp {
1904    let cur = xmlMallocImpl(core::mem::size_of::<_xsltElemPreComp>()) as *mut _xsltElemPreComp;
1905    if cur.is_null() {
1906        report_error(
1907            style,
1908            ptr::null_mut(),
1909            b"xsltNewExtElement : malloc failed\n",
1910        );
1911        return ptr::null_mut();
1912    }
1913    ptr::write_bytes(cur as *mut u8, 0, core::mem::size_of::<_xsltElemPreComp>());
1914
1915    xslt_init_elem_pre_comp(cur, style, inst, function, Some(xslt_free_elem_pre_comp));
1916
1917    cur
1918}
1919
1920/// Precompute an extension module element.
1921///
1922/// # UPSTREAM-PARITY
1923///
1924/// ```c
1925/// xsltElemPreCompPtr
1926/// xsltPreComputeExtModuleElement(xsltStylesheetPtr style, xmlNodePtr inst);
1927/// ```
1928///
1929/// Looks the element up in the extension-element registry by
1930/// `(inst->name, inst->ns->href)`; if the registered module provides a
1931/// precomputation callback it is used, otherwise a default
1932/// `_xsltElemPreComp` is created with the registered transform function.
1933///
1934/// # ENGINE-WIRING
1935///
1936/// The candidate mirror of upstream's global `xsltElementsHash` is the
1937/// private registry in this module (`XSLT_ELEMENTS_REGISTRY`). The
1938/// ext-family ABI (exports_xslt_ext.rs) owns the public registration
1939/// functions; this module's registry is populated through them by
1940/// whichever agent wires the module-level registration (documented
1941/// cross-family wire-up point). With an empty registry the function
1942/// returns NULL exactly like upstream with no registered elements.
1943///
1944/// # SAFETY
1945///
1946/// - `style` must be a valid `_xsltStylesheet`, or NULL.
1947/// - `inst` must be a valid element node, or NULL.
1948#[no_mangle]
1949pub unsafe extern "C" fn xsltPreComputeExtModuleElement(
1950    style: *mut _xsltStylesheet,
1951    inst: *mut _xmlNode,
1952) -> *mut _xsltElemPreComp {
1953    if style.is_null()
1954        || inst.is_null()
1955        || (*inst).type_ != XML_ELEMENT_NODE as c_int
1956        || (*inst).ns.is_null()
1957    {
1958        return ptr::null_mut();
1959    }
1960
1961    let ext = ext_element_lookup((*inst).name, (*(*inst).ns).href);
1962    if ext.is_null() {
1963        return ptr::null_mut();
1964    }
1965
1966    let mut comp: *mut _xsltElemPreComp = ptr::null_mut();
1967    if let Some(precomp) = (*ext).precomp {
1968        comp = precomp(style, inst, (*ext).transform) as *mut _xsltElemPreComp;
1969    }
1970    if comp.is_null() {
1971        // Default creation of an _xsltElemPreComp.
1972        comp = xslt_new_elem_pre_comp(style, inst, (*ext).transform);
1973    }
1974
1975    comp
1976}
1977
1978/// Free all precomputed blocks of a stylesheet.
1979///
1980/// # UPSTREAM-PARITY
1981///
1982/// ```c
1983/// void
1984/// xsltFreeStylePreComps(xsltStylesheetPtr style);
1985/// ```
1986///
1987/// Walks `style->preComps`; extension-typed precomps are released through
1988/// their registered deallocator, all others through `xsltFreeStylePreComp`
1989/// (which, in this engine, only frees the struct — no compiled
1990/// expressions or pattern lists exist).
1991///
1992/// # SAFETY
1993///
1994/// - `style` must be a valid `_xsltStylesheet`, or NULL.
1995#[no_mangle]
1996pub unsafe extern "C" fn xsltFreeStylePreComps(style: *mut _xsltStylesheet) {
1997    if style.is_null() {
1998        return;
1999    }
2000
2001    let mut cur = (*style).preComps as *mut _xsltElemPreComp;
2002    (*style).preComps = ptr::null_mut();
2003    while !cur.is_null() {
2004        let next = (*cur).next;
2005        if (*cur).type_ == XSLT_FUNC_EXTENSION {
2006            if let Some(free_func) = (*cur).free {
2007                free_func(cur as *mut c_void);
2008            } else {
2009                xslt_free_style_pre_comp(cur as *mut c_void);
2010            }
2011        } else {
2012            xslt_free_style_pre_comp(cur as *mut c_void);
2013        }
2014        cur = next;
2015    }
2016}
2017
2018/// Precompute an XSLT stylesheet element.
2019///
2020/// # UPSTREAM-PARITY
2021///
2022/// ```c
2023/// void
2024/// xsltStylePreCompute(xsltStylesheetPtr style, xmlNodePtr inst);
2025/// ```
2026///
2027/// Ported from preproc.c 1.1.45 (old behaviour): the grammar checks
2028/// (`xsltCheckTopLevelElement` / `xsltCheckInstructionElement` /
2029/// `xsltCheckParentElement`) and the per-instruction dispatch, including
2030/// the `xsl:document` precomp and the extension-element fallback
2031/// (`xsltPreComputeExtModuleElement`, else the `xsltExtMarker` sentinel).
2032///
2033/// # ENGINE-WIRING
2034///
2035/// The candidate engine compiles instructions lazily at transform time
2036/// from the raw node (it never reads `inst->psvi`), so the per-instruction
2037/// compilers allocate nothing; their observable effect — the error and
2038/// warning counters and the `style->preComps` chain for `xsl:document` and
2039/// extension elements — is preserved.
2040///
2041/// # SAFETY
2042///
2043/// - `style` must be a valid `_xsltStylesheet`, or NULL.
2044/// - `inst` must be a valid element node, or NULL.
2045#[no_mangle]
2046pub unsafe extern "C" fn xsltStylePreCompute(style: *mut _xsltStylesheet, inst: *mut _xmlNode) {
2047    if inst.is_null() || (*inst).type_ != XML_ELEMENT_NODE as c_int || !(*inst).psvi.is_null() {
2048        return;
2049    }
2050
2051    if is_xslt_elem(inst) {
2052        if is_xslt_name(inst, b"apply-templates") {
2053            xslt_check_instruction_element(style, inst);
2054            // xsltApplyTemplatesComp — lazy in this engine.
2055        } else if is_xslt_name(inst, b"with-param") {
2056            xslt_check_parent_element(style, inst, b"apply-templates", b"call-template\0".as_ptr() as *const u8);
2057            // xsltWithParamComp — lazy.
2058        } else if is_xslt_name(inst, b"value-of") {
2059            xslt_check_instruction_element(style, inst);
2060        } else if is_xslt_name(inst, b"copy") {
2061            xslt_check_instruction_element(style, inst);
2062        } else if is_xslt_name(inst, b"copy-of") {
2063            xslt_check_instruction_element(style, inst);
2064        } else if is_xslt_name(inst, b"if") {
2065            xslt_check_instruction_element(style, inst);
2066        } else if is_xslt_name(inst, b"when") {
2067            xslt_check_parent_element(style, inst, b"choose", ptr::null());
2068        } else if is_xslt_name(inst, b"choose") {
2069            xslt_check_instruction_element(style, inst);
2070        } else if is_xslt_name(inst, b"for-each") {
2071            xslt_check_instruction_element(style, inst);
2072        } else if is_xslt_name(inst, b"apply-imports") {
2073            xslt_check_instruction_element(style, inst);
2074        } else if is_xslt_name(inst, b"attribute") {
2075            let parent = (*inst).parent;
2076            let is_in_attr_set = !parent.is_null()
2077                && (*parent).type_ == XML_ELEMENT_NODE as c_int
2078                && !(*parent).ns.is_null()
2079                && xmlStrEqual(
2080                    (*(*parent).ns).href,
2081                    XSLT_NAMESPACE.as_ptr() as *const xmlChar,
2082                ) != 0
2083                && is_xslt_name(parent, b"attribute-set");
2084            if !is_in_attr_set {
2085                xslt_check_instruction_element(style, inst);
2086            }
2087            // xsltAttributeComp — lazy.
2088        } else if is_xslt_name(inst, b"element") {
2089            xslt_check_instruction_element(style, inst);
2090        } else if is_xslt_name(inst, b"text") {
2091            xslt_check_instruction_element(style, inst);
2092        } else if is_xslt_name(inst, b"sort") {
2093            xslt_check_parent_element(style, inst, b"apply-templates", b"for-each\0".as_ptr() as *const u8);
2094        } else if is_xslt_name(inst, b"comment") {
2095            xslt_check_instruction_element(style, inst);
2096        } else if is_xslt_name(inst, b"number") {
2097            xslt_check_instruction_element(style, inst);
2098        } else if is_xslt_name(inst, b"processing-instruction") {
2099            xslt_check_instruction_element(style, inst);
2100        } else if is_xslt_name(inst, b"call-template") {
2101            xslt_check_instruction_element(style, inst);
2102        } else if is_xslt_name(inst, b"param") {
2103            if xslt_check_top_level_element(style, inst, 0) == 0 {
2104                xslt_check_instruction_element(style, inst);
2105            }
2106            // xsltParamComp — lazy.
2107        } else if is_xslt_name(inst, b"variable") {
2108            if xslt_check_top_level_element(style, inst, 0) == 0 {
2109                xslt_check_instruction_element(style, inst);
2110            }
2111            // xsltVariableComp — lazy.
2112        } else if is_xslt_name(inst, b"otherwise") {
2113            xslt_check_parent_element(style, inst, b"choose", ptr::null());
2114            xslt_check_instruction_element(style, inst);
2115            return;
2116        } else if is_xslt_name(inst, b"template") {
2117            xslt_check_top_level_element(style, inst, 1);
2118            return;
2119        } else if is_xslt_name(inst, b"output") {
2120            xslt_check_top_level_element(style, inst, 1);
2121            return;
2122        } else if is_xslt_name(inst, b"preserve-space") {
2123            xslt_check_top_level_element(style, inst, 1);
2124            return;
2125        } else if is_xslt_name(inst, b"strip-space") {
2126            xslt_check_top_level_element(style, inst, 1);
2127            return;
2128        } else if is_xslt_name(inst, b"stylesheet") || is_xslt_name(inst, b"transform") {
2129            let parent = (*inst).parent;
2130            if parent.is_null() || (*parent).type_ != XML_DOCUMENT_NODE as c_int {
2131                report_error(style, inst, b"element only allowed only as root element\n");
2132                (*style).errors += 1;
2133            }
2134            return;
2135        } else if is_xslt_name(inst, b"key") {
2136            xslt_check_top_level_element(style, inst, 1);
2137            return;
2138        } else if is_xslt_name(inst, b"message") {
2139            xslt_check_instruction_element(style, inst);
2140            return;
2141        } else if is_xslt_name(inst, b"attribute-set") {
2142            xslt_check_top_level_element(style, inst, 1);
2143            return;
2144        } else if is_xslt_name(inst, b"namespace-alias") {
2145            xslt_check_top_level_element(style, inst, 1);
2146            return;
2147        } else if is_xslt_name(inst, b"include") {
2148            xslt_check_top_level_element(style, inst, 1);
2149            return;
2150        } else if is_xslt_name(inst, b"import") {
2151            xslt_check_top_level_element(style, inst, 1);
2152            return;
2153        } else if is_xslt_name(inst, b"decimal-format") {
2154            xslt_check_top_level_element(style, inst, 1);
2155            return;
2156        } else if is_xslt_name(inst, b"fallback") {
2157            xslt_check_instruction_element(style, inst);
2158            return;
2159        } else if is_xslt_name(inst, b"document") {
2160            xslt_check_instruction_element(style, inst);
2161            (*inst).psvi = xsltDocumentComp(style, inst, None) as *mut c_void;
2162        } else if style.is_null() || (*style).forwards_compatible == 0 {
2163            report_error(
2164                style,
2165                inst,
2166                b"xsltStylePreCompute: unknown xsl: instruction\n",
2167            );
2168            if !style.is_null() {
2169                (*style).warnings += 1;
2170            }
2171        }
2172    } else {
2173        // Unknown element: maybe an extension element registered at the
2174        // module level.
2175        (*inst).psvi = xsltPreComputeExtModuleElement(style, inst) as *mut c_void;
2176        if (*inst).psvi.is_null() {
2177            (*inst).psvi = XSLT_EXT_MARKER.as_ptr() as *mut c_void;
2178        }
2179    }
2180}
2181
2182/// `xsltCheckTopLevelElement` (preproc.c): check that the instruction is
2183/// instantiated as a top-level element.
2184///
2185/// Returns -1 on invalid args, 0 if the check failed, 1 on success.
2186unsafe fn xslt_check_top_level_element(
2187    style: *mut _xsltStylesheet,
2188    inst: *mut _xmlNode,
2189    err: c_int,
2190) -> c_int {
2191    if style.is_null() || inst.is_null() || (*inst).ns.is_null() {
2192        return -1;
2193    }
2194
2195    let parent = (*inst).parent;
2196    if parent.is_null() {
2197        if err != 0 {
2198            report_error(style, inst, b"internal problem: element has no parent\n");
2199            (*style).errors += 1;
2200        }
2201        return 0;
2202    }
2203    if (*parent).ns.is_null()
2204        || (*parent).type_ != XML_ELEMENT_NODE as c_int
2205        || (xmlStrEqual((*(*parent).ns).href, (*(*inst).ns).href) == 0)
2206        || (!is_xslt_name(parent, b"stylesheet") && !is_xslt_name(parent, b"transform"))
2207    {
2208        if err != 0 {
2209            report_error(
2210                style,
2211                inst,
2212                b"element only allowed as child of stylesheet\n",
2213            );
2214            (*style).errors += 1;
2215        }
2216        return 0;
2217    }
2218    1
2219}
2220
2221/// `xsltCheckInstructionElement` (preproc.c): check that the instruction
2222/// is instantiated as an instruction element.
2223unsafe fn xslt_check_instruction_element(style: *mut _xsltStylesheet, inst: *mut _xmlNode) {
2224    if style.is_null() || inst.is_null() || (*inst).ns.is_null() || (*style).literal_result != 0 {
2225        return;
2226    }
2227
2228    let has_ext = !(*style).extInfos.is_null() || !ext_ns_registered(ptr::null());
2229
2230    let mut parent = (*inst).parent;
2231    if parent.is_null() {
2232        report_error(style, inst, b"internal problem: element has no parent\n");
2233        (*style).errors += 1;
2234        return;
2235    }
2236    while !parent.is_null() && (*parent).type_ != XML_DOCUMENT_NODE as c_int {
2237        if ((*parent).ns == (*inst).ns
2238            || (!(*parent).ns.is_null()
2239                && xmlStrEqual((*(*parent).ns).href, (*(*inst).ns).href) != 0))
2240            && (is_xslt_name(parent, b"template")
2241                || is_xslt_name(parent, b"param")
2242                || is_xslt_name(parent, b"attribute")
2243                || is_xslt_name(parent, b"variable"))
2244        {
2245            return;
2246        }
2247
2248        // If we are within an extension element all bets are off about the
2249        // semantics there (e.g. xsl:param within func:function).
2250        if has_ext && !(*parent).ns.is_null() && ext_ns_registered((*(*parent).ns).href) {
2251            return;
2252        }
2253
2254        parent = (*parent).parent;
2255    }
2256    report_error(
2257        style,
2258        inst,
2259        b"element only allowed within a template, variable or param\n",
2260    );
2261    (*style).errors += 1;
2262}
2263
2264/// `xsltCheckParentElement` (preproc.c): check that the instruction is a
2265/// child of one of the possible parents.
2266unsafe fn xslt_check_parent_element(
2267    style: *mut _xsltStylesheet,
2268    inst: *mut _xmlNode,
2269    allow1: &[u8],
2270    allow2: *const u8,
2271) {
2272    if style.is_null() || inst.is_null() || (*inst).ns.is_null() || (*style).literal_result != 0 {
2273        return;
2274    }
2275
2276    let parent = (*inst).parent;
2277    if parent.is_null() {
2278        report_error(style, inst, b"internal problem: element has no parent\n");
2279        (*style).errors += 1;
2280        return;
2281    }
2282    let allow2_bytes: &[u8] = if allow2.is_null() {
2283        b""
2284    } else {
2285        core::slice::from_raw_parts(allow2, libc::strlen(allow2 as *const libc::c_char) as usize)
2286    };
2287    if ((*parent).ns == (*inst).ns
2288        || (!(*parent).ns.is_null() && xmlStrEqual((*(*parent).ns).href, (*(*inst).ns).href) != 0))
2289        && (is_xslt_name(parent, allow1)
2290            || (!allow2_bytes.is_empty() && is_xslt_name(parent, allow2_bytes)))
2291    {
2292        return;
2293    }
2294
2295    if !ext_ns_registered(ptr::null()) {
2296        let mut p = parent;
2297        while !p.is_null() && (*p).type_ != XML_DOCUMENT_NODE as c_int {
2298            if !(*p).ns.is_null() && ext_ns_registered((*(*p).ns).href) {
2299                return;
2300            }
2301            p = (*p).parent;
2302        }
2303    }
2304    report_error(style, inst, b"element is not allowed within that context\n");
2305    (*style).errors += 1;
2306}
2307
2308/// Normalize the compiled steps of an imported stylesheet (hash scanner
2309/// callback).
2310///
2311/// # UPSTREAM-PARITY
2312///
2313/// ```c
2314/// void xsltNormalizeCompSteps(void *payload,
2315///         void *data, const xmlChar *name ATTRIBUTE_UNUSED) {
2316///     xsltCompMatchPtr comp = payload;
2317///     xsltStylesheetPtr style = data;
2318///     for (ix = 0; ix < comp->nbStep; ix++) {
2319///         comp->steps[ix].previousExtra += style->extrasNr;
2320///         comp->steps[ix].indexExtra += style->extrasNr;
2321///         comp->steps[ix].lenExtra += style->extrasNr;
2322///     }
2323/// }
2324/// ```
2325///
2326/// # ENGINE-WIRING
2327///
2328/// Upstream's `xsltCompMatch` carries a step array with extra-slot
2329/// indices; the candidate's compiled pattern (`_xsltCompMatch` in
2330/// `exports_xslt_apply.rs`) is an opaque pointer with no step array, so
2331/// there is nothing to re-base — the function is a faithful no-op for the
2332/// candidate representation (documented divergence).
2333///
2334/// # SAFETY
2335///
2336/// - `payload` and `data` are only passed through (never dereferenced).
2337#[no_mangle]
2338pub unsafe extern "C" fn xsltNormalizeCompSteps(
2339    payload: *mut c_void,
2340    data: *mut c_void,
2341    _name: *const xmlChar,
2342) {
2343    let _ = (payload, data);
2344}
2345
2346// ═══════════════════════════════════════════════════════════════════════════════
2347// 6. Style documents (documents.c)
2348// ═══════════════════════════════════════════════════════════════════════════════
2349
2350/// Register a new stylesheet document (wrap it in an `_xsltDocument`).
2351///
2352/// # UPSTREAM-PARITY
2353///
2354/// ```c
2355/// xsltDocumentPtr
2356/// xsltNewStyleDocument(xsltStylesheetPtr style, xmlDocPtr doc) {
2357///     cur = xmlMallocImpl(sizeof(xsltDocument));
2358///     if (cur == NULL) { ... return(NULL); }
2359///     memset(cur, 0, sizeof(xsltDocument));
2360///     cur->doc = doc;
2361///     if (style != NULL) {
2362///         cur->next = style->docList;
2363///         style->docList = cur;
2364///     }
2365///     return(cur);
2366/// }
2367/// ```
2368///
2369/// The wrapper does NOT own `doc` (ownership stays with the caller or the
2370/// stylesheet's `doc` field).
2371///
2372/// # SAFETY
2373///
2374/// - `style` must be a valid `_xsltStylesheet`, or NULL.
2375/// - `doc` must be a valid parsed document.
2376#[no_mangle]
2377pub unsafe extern "C" fn xsltNewStyleDocument(
2378    style: *mut _xsltStylesheet,
2379    doc: *mut _xmlDoc,
2380) -> *mut _xsltDocument {
2381    let cur = xmlMallocImpl(core::mem::size_of::<_xsltDocument>()) as *mut _xsltDocument;
2382    if cur.is_null() {
2383        report_error(
2384            style,
2385            doc as *mut _xmlNode,
2386            b"xsltNewStyleDocument : malloc failed\n",
2387        );
2388        return ptr::null_mut();
2389    }
2390    ptr::write_bytes(cur as *mut u8, 0, core::mem::size_of::<_xsltDocument>());
2391    (*cur).doc = doc;
2392    if !style.is_null() {
2393        (*cur).next = (*style).docList;
2394        (*style).docList = cur;
2395    }
2396    cur
2397}
2398
2399/// Load a stylesheet document by URI, reusing an already-loaded document
2400/// from the stylesheet's doc list when possible.
2401///
2402/// # UPSTREAM-PARITY
2403///
2404/// ```c
2405/// xsltDocumentPtr
2406/// xsltLoadStyleDocument(xsltStylesheetPtr style, const xmlChar *URI);
2407/// ```
2408///
2409/// # ENGINE-WIRING
2410///
2411/// The default loader (documents.c `xsltDocDefaultLoaderFunc`) parses the
2412/// URI with `XSLT_PARSE_OPTIONS`; the candidate's loader mirrors
2413/// `src/xslt/documents` `load_via_loader` (registered loader first, else
2414/// `xmlReadFile`). On failure the freshly parsed document is freed.
2415///
2416/// # SAFETY
2417///
2418/// - `style` must be a valid `_xsltStylesheet`, or NULL.
2419/// - `URI` must be a valid NUL-terminated string, or NULL.
2420#[no_mangle]
2421pub unsafe extern "C" fn xsltLoadStyleDocument(
2422    style: *mut _xsltStylesheet,
2423    uri: *const xmlChar,
2424) -> *mut _xsltDocument {
2425    if style.is_null() || uri.is_null() {
2426        return ptr::null_mut();
2427    }
2428
2429    // Security framework check.
2430    let sec = crate::xslt::security::xsltGetDefaultSecurityPrefs();
2431    if !sec.is_null() {
2432        let res = xslt_check_read(sec, ptr::null_mut(), uri);
2433        if res <= 0 {
2434            if res == 0 {
2435                report_error(
2436                    ptr::null_mut(),
2437                    ptr::null_mut(),
2438                    b"xsltLoadStyleDocument: read rights for ",
2439                );
2440                report_error(ptr::null_mut(), ptr::null_mut(), cbytes(uri as *const u8));
2441                report_error(ptr::null_mut(), ptr::null_mut(), b" denied\n");
2442            }
2443            return ptr::null_mut();
2444        }
2445    }
2446
2447    // Walk the style's document list for a preparsed match.
2448    let mut ret = (*style).docList;
2449    while !ret.is_null() {
2450        if !(*ret).doc.is_null()
2451            && !(*(*ret).doc).URL.is_null()
2452            && xmlStrEqual((*(*ret).doc).URL, uri) != 0
2453        {
2454            return ret;
2455        }
2456        ret = (*ret).next;
2457    }
2458
2459    let doc = xslt_doc_default_loader(
2460        uri,
2461        (*style).dict,
2462        XSLT_PARSE_OPTIONS,
2463        style as *mut c_void,
2464        XSLT_LOAD_STYLESHEET,
2465    );
2466    if doc.is_null() {
2467        return ptr::null_mut();
2468    }
2469
2470    let ret = xsltNewStyleDocument(style, doc);
2471    if ret.is_null() {
2472        crate::xml::tree::free_doc(doc);
2473    }
2474    ret
2475}
2476
2477/// Free the node-trees (and `_xsltDocument` structures) of all
2478/// stylesheet-modules of the stylesheet-level represented by `style`.
2479///
2480/// # UPSTREAM-PARITY
2481///
2482/// ```c
2483/// void
2484/// xsltFreeStyleDocuments(xsltStylesheetPtr style) {
2485///     if (style == NULL) return;
2486///     cur = style->docList;
2487///     while (cur != NULL) {
2488///         doc = cur; cur = cur->next;
2489///         xsltFreeDocumentKeys(doc);
2490///         if (!doc->main) xmlFreeDoc(doc->doc);
2491///         xmlFreeImpl(doc);
2492///     }
2493/// }
2494/// ```
2495///
2496/// # ENGINE-WIRING
2497///
2498/// `xsltFreeDocumentKeys` (keys.c) frees the key tables cached on the
2499/// wrapper; the candidate computes keys on demand under the transform
2500/// context and caches them on the context's document wrapper, so nothing
2501/// is cached on style documents (documented divergence).
2502///
2503/// # SAFETY
2504///
2505/// - `style` must be a valid `_xsltStylesheet`, or NULL.
2506#[no_mangle]
2507pub unsafe extern "C" fn xsltFreeStyleDocuments(style: *mut _xsltStylesheet) {
2508    if style.is_null() {
2509        return;
2510    }
2511
2512    let mut cur = (*style).docList;
2513    (*style).docList = ptr::null_mut();
2514    while !cur.is_null() {
2515        let doc = cur;
2516        cur = (*cur).next;
2517        if (*doc).main == 0 && !(*doc).doc.is_null() {
2518            crate::xml::tree::free_doc((*doc).doc);
2519        }
2520        xmlFreeImpl(doc as *mut c_void);
2521    }
2522}
2523
2524// ═══════════════════════════════════════════════════════════════════════════════
2525// 7. Global state & extensions (extensions.c, xslt.c)
2526// ═══════════════════════════════════════════════════════════════════════════════
2527
2528/// Initialize the global variables for extensions.
2529///
2530/// # UPSTREAM-PARITY
2531///
2532/// ```c
2533/// void
2534/// xsltInitGlobals(void) {
2535///     if (xsltExtMutex == NULL) {
2536///         xsltExtMutex = xmlNewMutex();
2537///     }
2538/// }
2539/// ```
2540///
2541/// # ENGINE-WIRING
2542///
2543/// The candidate's global extension registries are plain statics that
2544/// require no lazy initialization; the call is kept as the idempotent
2545/// initialization marker (documented no-op mirroring upstream's
2546/// mutex-creation).
2547#[no_mangle]
2548pub unsafe extern "C" fn xsltInitGlobals() {
2549    if XSLT_GLOBALS_INITIALIZED == 0 {
2550        XSLT_GLOBALS_INITIALIZED = 1;
2551    }
2552}
2553
2554/// Uninitialize the processor.
2555///
2556/// # UPSTREAM-PARITY
2557///
2558/// ```c
2559/// void
2560/// xsltUninit (void) {
2561/// #ifdef XSLT_LOCALE_WINAPI
2562///     xmlFreeRMutex(xsltLocaleMutex);
2563///     xsltLocaleMutex = NULL;
2564/// #endif
2565///     initialized = 0;
2566/// }
2567/// ```
2568///
2569/// # ENGINE-WIRING
2570///
2571/// On the oracle (non-Win32) build this only clears the global
2572/// initialized flag; the candidate keeps process-lifetime statics, so the
2573/// observable behaviour is a no-op. The marker is reset for symmetry.
2574#[no_mangle]
2575pub unsafe extern "C" fn xsltUninit() {
2576    XSLT_GLOBALS_INITIALIZED = 0;
2577}
2578
2579/// Free the memory used by XSLT extensions in a stylesheet.
2580///
2581/// # UPSTREAM-PARITY
2582///
2583/// ```c
2584/// void
2585/// xsltFreeExts(xsltStylesheetPtr style) {
2586///     if (style->nsDefs != NULL)
2587///         xsltFreeExtDefList((xsltExtDefPtr) style->nsDefs);
2588/// }
2589/// ```
2590///
2591/// # ENGINE-WIRING
2592///
2593/// Upstream keeps the stylesheet's extension-prefix definitions in
2594/// `style->nsDefs`; the candidate *repurposes* `style->nsDefs` for the
2595/// preserve-space rule list (see `src/xslt/compiler`
2596/// `compile_space_rules`, documented divergence) and frees it as such in
2597/// `xsltFreeStylesheet`. No extension-prefix def list exists, so there is
2598/// nothing to free here — the function is an intentionally empty port
2599/// (freeing `nsDefs` again would double-free the preserve-space list).
2600///
2601/// # SAFETY
2602///
2603/// - `style` must be a valid `_xsltStylesheet`, or NULL.
2604#[no_mangle]
2605pub unsafe extern "C" fn xsltFreeExts(style: *mut _xsltStylesheet) {
2606    if style.is_null() {
2607        return;
2608    }
2609    // See ENGINE-WIRING above: nothing to free in the candidate engine.
2610}
2611
2612/// Shut down the set of extension modules loaded for a stylesheet.
2613///
2614/// # UPSTREAM-PARITY
2615///
2616/// ```c
2617/// void
2618/// xsltShutdownExts(xsltStylesheetPtr style) {
2619///     if (style == NULL) return;
2620///     if (style->extInfos == NULL) return;
2621///     xmlHashScan(style->extInfos, xsltShutdownExt, style);
2622///     xmlHashFree(style->extInfos, xsltFreeExtDataEntry);
2623///     style->extInfos = NULL;
2624/// }
2625/// ```
2626///
2627/// # ENGINE-WIRING
2628///
2629/// Upstream populates `style->extInfos` per stylesheet when a registered
2630/// module provides a style-init function; the candidate has no such
2631/// registration path, so `style->extInfos` is NULL for every stylesheet
2632/// and the function returns immediately — the exact upstream behaviour for
2633/// that state. If a hash were ever attached by an external writer, it is
2634/// released without invoking shutdown callbacks (no module metadata is
2635/// available to the compile family; documented divergence).
2636///
2637/// # SAFETY
2638///
2639/// - `style` must be a valid `_xsltStylesheet`, or NULL.
2640#[no_mangle]
2641pub unsafe extern "C" fn xsltShutdownExts(style: *mut _xsltStylesheet) {
2642    if style.is_null() {
2643        return;
2644    }
2645    if (*style).extInfos.is_null() {
2646        return;
2647    }
2648    // Unreachable in the candidate engine (extInfos is never populated);
2649    // free the table so the stylesheet teardown stays leak-free for
2650    // external writers.
2651    crate::xml::hash::hash_free((*style).extInfos as *mut crate::xml::hash::HashTable, None);
2652    (*style).extInfos = ptr::null_mut();
2653}
2654
2655/// Dump a list of the registered XSLT extension functions and elements.
2656///
2657/// # UPSTREAM-PARITY
2658///
2659/// ```c
2660/// void
2661/// xsltDebugDumpExtensions(FILE * output);
2662/// ```
2663///
2664/// Prints the same headings as extensions.c 1.1.45 to the given `FILE*`
2665/// (stdout when NULL): the extension-function and top-level registries do
2666/// not exist in the candidate (always "No registered …"), while the
2667/// instruction-element and module registries print their entries
2668/// (`{URI}name` and `URI` lines respectively).
2669///
2670/// # SAFETY
2671///
2672/// - `output` must be a valid `FILE*`, or NULL (stdout).
2673#[no_mangle]
2674pub unsafe extern "C" fn xsltDebugDumpExtensions(output: *mut libc::FILE) {
2675    let out = if output.is_null() {
2676        libc::fdopen(1, b"w\0".as_ptr() as *const c_char)
2677    } else {
2678        output
2679    };
2680
2681    if out.is_null() {
2682        return;
2683    }
2684
2685    libc::fprintf(
2686        out,
2687        b"Registered XSLT Extensions\n--------------------------\n\0".as_ptr() as *const c_char,
2688    );
2689    libc::fprintf(
2690        out,
2691        b"No registered extension functions\n\0".as_ptr() as *const c_char,
2692    );
2693    libc::fprintf(
2694        out,
2695        b"\nNo registered top-level extension elements\n\0".as_ptr() as *const c_char,
2696    );
2697
2698    if XSLT_ELEMENTS_REGISTRY.is_null() {
2699        libc::fprintf(
2700            out,
2701            b"\nNo registered instruction extension elements\n\0".as_ptr() as *const c_char,
2702        );
2703    } else {
2704        libc::fprintf(
2705            out,
2706            b"\nRegistered instruction extension elements:\n\0".as_ptr() as *const c_char,
2707        );
2708        let mut cur = XSLT_ELEMENTS_REGISTRY;
2709        while !cur.is_null() {
2710            if !(*cur).URI.is_null() && !(*cur).name.is_null() {
2711                libc::fprintf(
2712                    out,
2713                    b"{%s}%s\n\0".as_ptr() as *const c_char,
2714                    (*cur).URI as *const c_char,
2715                    (*cur).name as *const c_char,
2716                );
2717            }
2718            cur = (*cur).next;
2719        }
2720    }
2721
2722    if XSLT_MODULES_REGISTRY.is_null() {
2723        libc::fprintf(
2724            out,
2725            b"\nNo registered extension modules\n\0".as_ptr() as *const c_char,
2726        );
2727    } else {
2728        libc::fprintf(
2729            out,
2730            b"\nRegistered extension modules:\n\0".as_ptr() as *const c_char,
2731        );
2732        let mut cur = XSLT_MODULES_REGISTRY;
2733        while !cur.is_null() {
2734            if !(*cur).URI.is_null() {
2735                libc::fprintf(
2736                    out,
2737                    b"%s\n\0".as_ptr() as *const c_char,
2738                    (*cur).URI as *const c_char,
2739                );
2740            }
2741            cur = (*cur).next;
2742        }
2743    }
2744}