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