Skip to main content

libxml_rs/xml/schematron/
mod.rs

1//! Schematron implementation (§27, §85 Phase 6).
2//!
3//! ISO Schematron validation support. libxml2 implements a subset
4//! (Schematron 1.x style).
5//!
6//! Phase 6: Complete — Schematron schema parsing, document validation,
7//! and C ABI exports are implemented.
8//!
9//! # UPSTREAM-PARITY
10//!
11//! This module implements a functional ISO Schematron validator following
12//! libxml2's observable behavior. The implementation covers:
13//!
14//! - Full XML syntax for ISO Schematron schema definitions
15//! - Assert (`<assert>`) and report (`<report>`) pattern validation
16//! - XPath context matching via rule `context` attributes
17//! - Abstract rules and `extends` inheritance
18//! - Phases with `<active>` pattern references
19//! - Namespace prefix mappings via `<ns>`
20//! - Diagnostic messages with `<name/>` and `<value-of/>` expansion
21//! - Basic `<include>` support
22//! - `<let>`, `<param>`, `<diagnostics>`, `<diagnostic>`, `<dir>`, `<span>`, `<emph>`,
23//!   `<p>`, `<caption>` parsing
24//!
25//! Deviations from the ISO Schematron specification that match libxml2's
26//! behavior are intentional.
27
28#![allow(
29    missing_docs,
30    non_snake_case,
31    non_camel_case_types,
32    non_upper_case_globals
33)]
34
35use core::ffi::c_void;
36use core::ptr;
37use std::collections::HashMap;
38use std::os::raw::{c_char, c_int};
39
40use crate::abi::allocator;
41use crate::abi::structs::*;
42use crate::abi::types::xmlElementType::*;
43use crate::abi::types::*;
44use crate::xml::xpath::ast::CompiledExpr;
45use crate::xml::xpath::context::XPathContext;
46use crate::xml::xpath::types::XPathValue;
47
48// ═══════════════════════════════════════════════════════════════════════════════
49// Schematron Pattern Types
50// ═══════════════════════════════════════════════════════════════════════════════
51
52/// The type of a Schematron pattern (assert or report).
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum SchematronPatternType {
55    /// `<assert>` — the test must be true
56    Assert,
57    /// `<report>` — the test must be false
58    Report,
59}
60
61/// A single Schematron pattern — an assert or report assertion.
62///
63/// # UPSTREAM-PARITY
64///
65/// Mirrors libxml2's internal schematron pattern representation.
66#[derive(Debug, Clone)]
67pub struct SchematronPattern {
68    /// The type of this pattern (assert or report).
69    pub pattern_type: SchematronPatternType,
70    /// XPath test expression string.
71    pub test: String,
72    /// Compiled XPath test expression.
73    pub compiled_test: Option<CompiledExpr>,
74    /// Diagnostic message text (may contain `<name/>` and `<value-of/>`).
75    pub text: String,
76    /// Flag/severity attribute.
77    pub flag: Option<String>,
78    /// Role attribute.
79    pub role: Option<String>,
80    /// ID attribute.
81    pub id: Option<String>,
82    /// Icon attribute.
83    pub icon: Option<String>,
84    /// See attribute.
85    pub see: Option<String>,
86    /// Diagnostics reference.
87    pub diagnostics: Option<String>,
88}
89
90impl SchematronPattern {
91    /// Create a new assert or report pattern.
92    pub fn new(pattern_type: SchematronPatternType, test: String, text: String) -> Self {
93        Self {
94            pattern_type,
95            compiled_test: crate::xml::xpath::compile(&test),
96            test,
97            text,
98            flag: None,
99            role: None,
100            id: None,
101            icon: None,
102            see: None,
103            diagnostics: None,
104        }
105    }
106}
107
108// ═══════════════════════════════════════════════════════════════════════════════
109// Schematron Rule
110// ═══════════════════════════════════════════════════════════════════════════════
111
112/// A Schematron rule — matches nodes via a context XPath expression.
113///
114/// # UPSTREAM-PARITY
115///
116/// Mirrors libxml2's `xmlSchematronRule` internal structure.
117#[derive(Debug, Clone)]
118pub struct SchematronRule {
119    /// XPath context expression.
120    pub context: String,
121    /// Compiled context XPath expression.
122    pub compiled_context: Option<CompiledExpr>,
123    /// Assertions and reports in this rule.
124    pub patterns: Vec<SchematronPattern>,
125    /// Rule ID.
126    pub id: Option<String>,
127    /// Whether this rule is abstract.
128    pub abstract_: bool,
129    /// Extended rules (from `<extends>`).
130    pub extends: Vec<String>,
131}
132
133impl SchematronRule {
134    /// Create a new rule with the given context.
135    pub fn new(context: String) -> Self {
136        Self {
137            compiled_context: crate::xml::xpath::compile(&context),
138            context,
139            patterns: Vec::new(),
140            id: None,
141            abstract_: false,
142            extends: Vec::new(),
143        }
144    }
145}
146
147// ═══════════════════════════════════════════════════════════════════════════════
148// Schematron Phase
149// ═══════════════════════════════════════════════════════════════════════════════
150
151/// A Schematron phase — selects a subset of patterns for validation.
152///
153/// # UPSTREAM-PARITY
154///
155/// Mirrors libxml2's phase representation.
156#[derive(Debug, Clone)]
157pub struct SchematronPhase {
158    /// Phase ID.
159    pub id: String,
160    /// Active pattern IDs referenced by this phase.
161    pub active_patterns: Vec<String>,
162}
163
164// ═══════════════════════════════════════════════════════════════════════════════
165// Schematron Diagnostic
166// ═══════════════════════════════════════════════════════════════════════════════
167
168/// A Schematron diagnostic message definition.
169#[derive(Debug, Clone)]
170pub struct SchematronDiagnostic {
171    /// Diagnostic ID.
172    pub id: String,
173    /// Message text.
174    pub text: String,
175    /// Icon attribute.
176    pub icon: Option<String>,
177    /// See attribute.
178    pub see: Option<String>,
179}
180
181// ═══════════════════════════════════════════════════════════════════════════════
182// Schematron Schema
183// ═══════════════════════════════════════════════════════════════════════════════
184
185/// A compiled ISO Schematron schema.
186///
187/// # UPSTREAM-PARITY
188///
189/// Mirrors libxml2's `xmlSchematron` internal structure.
190#[derive(Debug, Clone)]
191pub struct SchematronSchema {
192    /// Schema title.
193    pub title: Option<String>,
194    /// Named phases (empty = use default phase = all rules).
195    pub phases: HashMap<String, SchematronPhase>,
196    /// All defined rules, keyed by ID.
197    pub rules: HashMap<String, SchematronRule>,
198    /// Pattern-level grouping: pattern ID -> list of rule IDs.
199    pub pattern_groups: HashMap<String, Vec<String>>,
200    /// Order of pattern groups (preserves document order).
201    pub pattern_order: Vec<String>,
202    /// Namespace prefix mappings.
203    pub ns: HashMap<String, String>,
204    /// Query binding (default "xslt").
205    pub query_binding: String,
206    /// Default phase (if phases exist, the first one is default).
207    pub default_phase: Option<String>,
208    /// Diagnostics definitions.
209    pub diagnostics: HashMap<String, SchematronDiagnostic>,
210    /// Errors encountered during parsing.
211    pub errors: Vec<String>,
212}
213
214impl SchematronSchema {
215    /// Create a new empty Schematron schema.
216    pub fn new() -> Self {
217        Self {
218            title: None,
219            phases: HashMap::new(),
220            rules: HashMap::new(),
221            pattern_groups: HashMap::new(),
222            pattern_order: Vec::new(),
223            ns: HashMap::new(),
224            query_binding: "xslt".to_string(),
225            default_phase: None,
226            diagnostics: HashMap::new(),
227            errors: Vec::new(),
228        }
229    }
230
231    /// Resolve a rule by ID, following `extends` chains.
232    pub fn resolve_rule(&self, rule_id: &str) -> Option<SchematronRule> {
233        let rule = self.rules.get(rule_id)?.clone();
234        Some(self.resolve_extends(rule))
235    }
236
237    /// Resolve `extends` for a rule, merging patterns from extended rules.
238    fn resolve_extends(&self, mut rule: SchematronRule) -> SchematronRule {
239        let extended_ids: Vec<String> = rule.extends.clone();
240        for ext_id in &extended_ids {
241            if let Some(ext_rule) = self.rules.get(ext_id) {
242                // Inherit patterns from the extended rule (abstract rules)
243                let resolved_ext = self.resolve_extends(ext_rule.clone());
244                rule.patterns.extend(resolved_ext.patterns);
245            }
246        }
247        rule
248    }
249
250    /// Get the active rules for a given phase.
251    /// If `phase_id` is None, returns all non-abstract rules.
252    /// If a phase is specified, returns only rules referenced by that phase's active patterns.
253    pub fn active_rules(&self, phase_id: Option<&str>) -> Vec<SchematronRule> {
254        // Determine which pattern IDs are active
255        let active_patterns: Vec<String> = match phase_id {
256            Some(pid) => {
257                if let Some(phase) = self.phases.get(pid) {
258                    phase.active_patterns.clone()
259                } else {
260                    // Unknown phase — use all patterns
261                    self.pattern_order.clone()
262                }
263            }
264            None => {
265                // Default phase: if phases exist, use first phase; otherwise all patterns
266                if let Some(default) = &self.default_phase {
267                    if let Some(phase) = self.phases.get(default) {
268                        phase.active_patterns.clone()
269                    } else {
270                        self.pattern_order.clone()
271                    }
272                } else {
273                    self.pattern_order.clone()
274                }
275            }
276        };
277
278        let mut result = Vec::new();
279        for pat_id in &active_patterns {
280            if let Some(rule_ids) = self.pattern_groups.get(pat_id) {
281                for rule_id in rule_ids {
282                    if let Some(rule) = self.rules.get(rule_id) {
283                        if !rule.abstract_ {
284                            result.push(self.resolve_extends(rule.clone()));
285                        }
286                    }
287                }
288            }
289        }
290
291        result
292    }
293}
294
295impl Default for SchematronSchema {
296    fn default() -> Self {
297        Self::new()
298    }
299}
300
301// ═══════════════════════════════════════════════════════════════════════════════
302// Schematron Validation Context
303// ═══════════════════════════════════════════════════════════════════════════════
304
305/// Validation context for Schematron schema validation.
306///
307/// Tracks errors and state during validation of an XML document against
308/// a Schematron schema. Mirrors libxml2's `xmlSchematronValidCtxt`.
309#[derive(Debug)]
310pub struct SchematronValidCtxt {
311    /// The schema being validated against.
312    pub schema: Option<SchematronSchema>,
313    /// Accumulated validation errors.
314    pub errors: Vec<String>,
315    /// Number of validation errors.
316    pub nb_errors: i32,
317    /// Active phase ID (None = use default phase).
318    pub active_phase: Option<String>,
319}
320
321impl SchematronValidCtxt {
322    /// Create a new validation context.
323    pub fn new() -> Self {
324        Self {
325            schema: None,
326            errors: Vec::new(),
327            nb_errors: 0,
328            active_phase: None,
329        }
330    }
331
332    /// Record a validation error.
333    pub fn record_error(&mut self, msg: String) {
334        self.errors.push(msg);
335        self.nb_errors += 1;
336    }
337}
338
339impl Default for SchematronValidCtxt {
340    fn default() -> Self {
341        Self::new()
342    }
343}
344
345// ═══════════════════════════════════════════════════════════════════════════════
346// Internal helpers
347// ═══════════════════════════════════════════════════════════════════════════════
348
349/// Get the local name of an element node (strip namespace prefix).
350///
351/// # SAFETY
352///
353/// - `node` must be a valid pointer to an _xmlNode or NULL.
354unsafe fn get_local_name(node: *mut _xmlNode) -> String {
355    if node.is_null() {
356        return String::new();
357    }
358    unsafe {
359        let name = (*node).name;
360        if name.is_null() {
361            return String::new();
362        }
363        let mut len = 0;
364        while *name.add(len) != 0 {
365            len += 1;
366        }
367        let slice = std::slice::from_raw_parts(name, len);
368        if let Ok(s) = std::str::from_utf8(slice) {
369            if let Some(pos) = s.find(':') {
370                s[pos + 1..].to_string()
371            } else {
372                s.to_string()
373            }
374        } else {
375            String::new()
376        }
377    }
378}
379
380/// Get the qualified name of a node (with namespace prefix if available).
381///
382/// # SAFETY
383///
384/// - `node` must be a valid pointer to an _xmlNode or NULL.
385unsafe fn get_node_qname(node: *mut _xmlNode) -> String {
386    if node.is_null() {
387        return String::new();
388    }
389    unsafe {
390        let ns = (*node).ns;
391        let prefix = if !ns.is_null() && !(*ns).prefix.is_null() {
392            let mut len = 0;
393            while *(*ns).prefix.add(len) != 0 {
394                len += 1;
395            }
396            let slice = std::slice::from_raw_parts((*ns).prefix, len);
397            if let Ok(s) = std::str::from_utf8(slice) {
398                format!("{}:", s)
399            } else {
400                String::new()
401            }
402        } else {
403            String::new()
404        };
405
406        let name = (*node).name;
407        if name.is_null() {
408            return String::new();
409        }
410        let mut len = 0;
411        while *name.add(len) != 0 {
412            len += 1;
413        }
414        let slice = std::slice::from_raw_parts(name, len);
415        if let Ok(s) = std::str::from_utf8(slice) {
416            format!("{}{}", prefix, s)
417        } else {
418            String::new()
419        }
420    }
421}
422
423/// Get the text content of an xmlNode (recursively collects text children).
424///
425/// # SAFETY
426///
427/// - `node` must be a valid pointer to an _xmlNode or NULL.
428unsafe fn get_node_text(node: *mut _xmlNode) -> String {
429    if node.is_null() {
430        return String::new();
431    }
432    let mut result = String::new();
433    unsafe {
434        let mut child = (*node).children;
435        while !child.is_null() {
436            if (*child).type_ == XML_TEXT_NODE as c_int
437                || (*child).type_ == XML_CDATA_SECTION_NODE as c_int
438            {
439                if !(*child).content.is_null() {
440                    let content = (*child).content;
441                    let mut len = 0;
442                    while *content.add(len) != 0 {
443                        len += 1;
444                    }
445                    let slice = std::slice::from_raw_parts(content, len);
446                    result.push_str(&String::from_utf8_lossy(slice));
447                }
448            }
449            child = (*child).next;
450        }
451    }
452    result
453}
454
455/// Get an attribute value from an xmlNode.
456///
457/// # SAFETY
458///
459/// - `node` must be a valid pointer to an _xmlNode or NULL.
460unsafe fn get_attr(node: *mut _xmlNode, name: &str) -> Option<String> {
461    if node.is_null() {
462        return None;
463    }
464    unsafe {
465        let mut prop = (*node).properties;
466        while !prop.is_null() {
467            let prop_name = (*prop).name;
468            if !prop_name.is_null() {
469                let mut len = 0;
470                while *prop_name.add(len) != 0 {
471                    len += 1;
472                }
473                let slice = std::slice::from_raw_parts(prop_name, len);
474                if let Ok(s) = std::str::from_utf8(slice) {
475                    if s == name {
476                        return Some(get_node_text(prop as *mut _xmlNode));
477                    }
478                }
479            }
480            prop = (*prop).next;
481        }
482    }
483    None
484}
485
486/// Check if an xmlNode is an element with a given local name.
487///
488/// # SAFETY
489///
490/// - `node` must be a valid pointer to an _xmlNode or NULL.
491unsafe fn node_is(node: *mut _xmlNode, local_name: &str) -> bool {
492    if node.is_null() {
493        return false;
494    }
495    unsafe {
496        let name = (*node).name;
497        if name.is_null() {
498            return false;
499        }
500        let mut len = 0;
501        while *name.add(len) != 0 {
502            len += 1;
503        }
504        let slice = std::slice::from_raw_parts(name, len);
505        if let Ok(s) = std::str::from_utf8(slice) {
506            let local = if let Some(pos) = s.find(':') {
507                &s[pos + 1..]
508            } else {
509                s
510            };
511            return local == local_name;
512        }
513    }
514    false
515}
516
517/// Get all child elements of a node.
518///
519/// # SAFETY
520///
521/// - `node` must be a valid pointer to an _xmlNode or NULL.
522unsafe fn child_elements(node: *mut _xmlNode) -> Vec<*mut _xmlNode> {
523    let mut children = Vec::new();
524    if node.is_null() {
525        return children;
526    }
527    unsafe {
528        let mut child = (*node).children;
529        while !child.is_null() {
530            if (*child).type_ == XML_ELEMENT_NODE as c_int {
531                children.push(child);
532            }
533            child = (*child).next;
534        }
535    }
536    children
537}
538
539/// Get the text content of an element, including inline elements (span, emph).
540/// Extracts all text content recursively.
541///
542/// # SAFETY
543///
544/// - `node` must be a valid pointer to an _xmlNode or NULL.
545unsafe fn get_inline_text(node: *mut _xmlNode) -> String {
546    if node.is_null() {
547        return String::new();
548    }
549    let mut result = String::new();
550    unsafe {
551        let mut child = (*node).children;
552        while !child.is_null() {
553            if (*child).type_ == XML_TEXT_NODE as c_int
554                || (*child).type_ == XML_CDATA_SECTION_NODE as c_int
555            {
556                if !(*child).content.is_null() {
557                    let content = (*child).content;
558                    let mut len = 0;
559                    while *content.add(len) != 0 {
560                        len += 1;
561                    }
562                    let slice = std::slice::from_raw_parts(content, len);
563                    result.push_str(&String::from_utf8_lossy(slice));
564                }
565            } else if (*child).type_ == XML_ELEMENT_NODE as c_int {
566                let local = get_local_name(child);
567                match local.as_str() {
568                    "span" | "emph" | "dir" => {
569                        result.push_str(&get_inline_text(child));
570                    }
571                    _ => {}
572                }
573            }
574            child = (*child).next;
575        }
576    }
577    result
578}
579
580// ═══════════════════════════════════════════════════════════════════════════════
581// Schematron Schema Parsing
582// ═══════════════════════════════════════════════════════════════════════════════
583
584/// Parse a Schematron schema from an XML string.
585///
586/// # UPSTREAM-PARITY
587///
588/// Equivalent to `xmlSchematronParse` in libxml2 when given a parser context
589/// created from a memory buffer.
590///
591/// Returns the parsed schema, or an error message on failure.
592pub fn schematron_parse(xml_doc: &str) -> Result<SchematronSchema, String> {
593    let doc_ptr = unsafe {
594        crate::abi::exports_xml2::xmlReadMemory(
595            xml_doc.as_ptr() as *const c_char,
596            xml_doc.len() as c_int,
597            b"schema.sch\0".as_ptr() as *const c_char,
598            ptr::null(),
599            0,
600        )
601    };
602
603    if doc_ptr.is_null() {
604        return Err("Failed to parse Schematron schema XML document".to_string());
605    }
606
607    let result = unsafe { schematron_parse_doc(doc_ptr) };
608    unsafe {
609        crate::abi::exports_xml2::xmlFreeDoc(doc_ptr);
610    }
611    result
612}
613
614/// Parse a Schematron schema from a parsed XML document.
615///
616/// # SAFETY
617///
618/// - `doc` must be a valid pointer to an _xmlDoc representing a Schematron schema.
619unsafe fn schematron_parse_doc(doc: *mut _xmlDoc) -> Result<SchematronSchema, String> {
620    unsafe {
621        let root = (*doc).children;
622        if root.is_null() {
623            return Err("Schematron document has no root element".to_string());
624        }
625
626        // Find the root element (skip non-element nodes)
627        let mut root_elem = root;
628        while !root_elem.is_null() && (*root_elem).type_ != XML_ELEMENT_NODE as c_int {
629            root_elem = (*root_elem).next;
630        }
631
632        if root_elem.is_null() {
633            return Err("Schematron document has no root element".to_string());
634        }
635
636        let local_name = get_local_name(root_elem);
637        if local_name != "schema" {
638            return Err(format!(
639                "Expected '<schema>' root element, found '<{}>'",
640                local_name
641            ));
642        }
643
644        Ok(schematron_parse_schema_node(root_elem))
645    }
646}
647
648/// Parse a `<schema>` element.
649///
650/// # SAFETY
651///
652/// - `node` must be a valid pointer to a `<schema>` element node.
653unsafe fn schematron_parse_schema_node(node: *mut _xmlNode) -> SchematronSchema {
654    unsafe {
655        let mut schema = SchematronSchema::new();
656
657        // Parse attributes
658        if let Some(qb) = get_attr(node, "queryBinding") {
659            schema.query_binding = qb;
660        }
661        schema.title = get_attr(node, "title");
662        if let Some(df) = get_attr(node, "defaultPhase") {
663            schema.default_phase = Some(df);
664        }
665
666        // Parse child elements
667        let mut current_pattern_id: Option<String> = None;
668        let mut pattern_names: HashMap<String, Vec<String>> = HashMap::new();
669
670        let mut child = (*node).children;
671        while !child.is_null() {
672            if (*child).type_ == XML_ELEMENT_NODE as c_int {
673                let local = get_local_name(child);
674                match local.as_str() {
675                    "title" => {
676                        if schema.title.is_none() {
677                            schema.title = Some(get_node_text(child).trim().to_string());
678                        }
679                    }
680                    "ns" => {
681                        let prefix = get_attr(child, "prefix").unwrap_or_default();
682                        let uri = get_attr(child, "uri").unwrap_or_default();
683                        if !prefix.is_empty() && !uri.is_empty() {
684                            schema.ns.insert(prefix, uri);
685                        }
686                    }
687                    "phase" => {
688                        let phase = schematron_parse_phase(child);
689                        schema.phases.insert(phase.id.clone(), phase);
690                    }
691                    "pattern" => {
692                        let pat_id = schematron_parse_pattern_node(
693                            child,
694                            &mut schema,
695                            &mut current_pattern_id,
696                            &mut pattern_names,
697                        );
698                        current_pattern_id = pat_id;
699                    }
700                    "rule" => {
701                        // Rule directly inside schema (not inside a pattern)
702                        let rule = schematron_parse_rule(child, &mut schema);
703                        let rule_id = rule
704                            .id
705                            .clone()
706                            .unwrap_or_else(|| format!("_rule_{}", schema.rules.len()));
707                        // Store the rule
708                        let rid = rule_id.clone();
709                        schema.rules.insert(rid, rule);
710
711                        // If we're inside a pattern, associate this rule with it
712                        if let Some(ref pid) = current_pattern_id {
713                            schema
714                                .pattern_groups
715                                .entry(pid.clone())
716                                .or_insert_with(Vec::new)
717                                .push(rule_id);
718                        } else {
719                            // No current pattern — create an anonymous pattern group
720                            let anon_id = format!("_anon_{}", schema.pattern_order.len());
721                            schema
722                                .pattern_groups
723                                .entry(anon_id.clone())
724                                .or_insert_with(Vec::new)
725                                .push(rule_id);
726                            if !schema.pattern_order.contains(&anon_id) {
727                                schema.pattern_order.push(anon_id);
728                            }
729                        }
730                    }
731                    "diagnostics" => {
732                        schematron_parse_diagnostics(child, &mut schema);
733                    }
734                    "include" => {
735                        schematron_parse_include(child, &mut schema);
736                    }
737                    "p" | "caption" => {
738                        // Documentation elements — skip
739                    }
740                    _ => {
741                        schema
742                            .errors
743                            .push(format!("Unexpected element '<{}>' in schema", local));
744                    }
745                }
746            }
747            child = (*child).next;
748        }
749
750        schema
751    }
752}
753
754/// Parse a `<pattern>` element.
755///
756/// # SAFETY
757///
758/// - `node` must be a valid pointer to a `<pattern>` element node.
759unsafe fn schematron_parse_pattern_node(
760    node: *mut _xmlNode,
761    schema: &mut SchematronSchema,
762    _current_pattern_id: &mut Option<String>,
763    _pattern_names: &mut HashMap<String, Vec<String>>,
764) -> Option<String> {
765    unsafe {
766        let pat_id = get_attr(node, "id");
767        let pat_name = get_attr(node, "name");
768        let pat_is_a = get_attr(node, "is-a");
769        let pat_see = get_attr(node, "see");
770        let pat_icon = get_attr(node, "icon");
771        let pat_role = get_attr(node, "role");
772
773        let pid = pat_id
774            .clone()
775            .unwrap_or_else(|| format!("_pattern_{}", schema.pattern_order.len()));
776
777        let mut rule_ids: Vec<String> = Vec::new();
778
779        // Parse child elements
780        let mut child = (*node).children;
781        while !child.is_null() {
782            if (*child).type_ == XML_ELEMENT_NODE as c_int {
783                let local = get_local_name(child);
784                match local.as_str() {
785                    "rule" => {
786                        let rule = schematron_parse_rule(child, schema);
787                        let rule_id = rule
788                            .id
789                            .clone()
790                            .unwrap_or_else(|| format!("_rule_{}", schema.rules.len()));
791                        let rid = rule_id.clone();
792                        schema.rules.insert(rid, rule);
793                        rule_ids.push(rule_id);
794                    }
795                    "p" | "caption" => {
796                        // Documentation — skip
797                    }
798                    _ => {
799                        schema
800                            .errors
801                            .push(format!("Unexpected element '<{}>' in pattern", local));
802                    }
803                }
804            }
805            child = (*child).next;
806        }
807
808        schema.pattern_groups.insert(pid.clone(), rule_ids);
809        schema.pattern_order.push(pid.clone());
810
811        // For is-a patterns, we store the reference but don't resolve here
812        if pat_is_a.is_some() {
813            // Pattern inherits from another pattern — store reference info on the pattern
814            // In a full implementation, this would merge rules from the referenced pattern
815        }
816
817        // Store metadata on the pattern group (could add a separate metadata map)
818        let _ = pat_name;
819        let _ = pat_see;
820        let _ = pat_icon;
821        let _ = pat_role;
822
823        Some(pid)
824    }
825}
826
827/// Parse a `<rule>` element.
828///
829/// # SAFETY
830///
831/// - `node` must be a valid pointer to a `<rule>` element node.
832unsafe fn schematron_parse_rule(
833    node: *mut _xmlNode,
834    schema: &mut SchematronSchema,
835) -> SchematronRule {
836    unsafe {
837        let context = get_attr(node, "context").unwrap_or_default();
838        let mut rule = SchematronRule::new(context);
839        rule.id = get_attr(node, "id");
840
841        let abs = get_attr(node, "abstract").unwrap_or_default();
842        rule.abstract_ = abs == "true" || abs == "1";
843
844        // Parse child elements
845        let mut child = (*node).children;
846        while !child.is_null() {
847            if (*child).type_ == XML_ELEMENT_NODE as c_int {
848                let local = get_local_name(child);
849                match local.as_str() {
850                    "assert" => {
851                        let pattern = schematron_parse_assert(child, SchematronPatternType::Assert);
852                        rule.patterns.push(pattern);
853                    }
854                    "report" => {
855                        let pattern = schematron_parse_assert(child, SchematronPatternType::Report);
856                        rule.patterns.push(pattern);
857                    }
858                    "extends" => {
859                        if let Some(ext_rule) = get_attr(child, "rule") {
860                            rule.extends.push(ext_rule);
861                        }
862                    }
863                    "let" => {
864                        // <let> defines a variable — we store it on the schema for now
865                        // (simplified — in a full implementation this would be scoped)
866                        let name = get_attr(child, "name").unwrap_or_default();
867                        let value = get_attr(child, "value").unwrap_or_default();
868                        if !name.is_empty() {
869                            // Store let binding on the rule context
870                            // For now, we just skip since we don't have variable evaluation
871                            let _ = value;
872                        }
873                    }
874                    "param" => {
875                        // Simplified: param is used for schema parameters
876                        let _name = get_attr(child, "name");
877                        let _value = get_attr(child, "value");
878                    }
879                    "p" | "caption" => {
880                        // Documentation — skip
881                    }
882                    _ => {
883                        schema
884                            .errors
885                            .push(format!("Unexpected element '<{}>' in rule", local));
886                    }
887                }
888            }
889            child = (*child).next;
890        }
891
892        rule
893    }
894}
895
896/// Parse an `<assert>` or `<report>` element.
897///
898/// # SAFETY
899///
900/// - `node` must be a valid pointer to an `<assert>` or `<report>` element node.
901unsafe fn schematron_parse_assert(
902    node: *mut _xmlNode,
903    pattern_type: SchematronPatternType,
904) -> SchematronPattern {
905    unsafe {
906        let test = get_attr(node, "test").unwrap_or_default();
907        let text = get_inline_text(node);
908
909        let mut pattern = SchematronPattern::new(pattern_type, test, text);
910        pattern.flag = get_attr(node, "flag");
911        pattern.id = get_attr(node, "id");
912        pattern.icon = get_attr(node, "icon");
913        pattern.see = get_attr(node, "see");
914        pattern.role = get_attr(node, "role");
915        pattern.diagnostics = get_attr(node, "diagnostics");
916
917        // Check for <name> and <value-of> children — these are handled during
918        // message expansion in the validator
919
920        pattern
921    }
922}
923
924/// Parse a `<phase>` element.
925///
926/// # SAFETY
927///
928/// - `node` must be a valid pointer to a `<phase>` element node.
929unsafe fn schematron_parse_phase(node: *mut _xmlNode) -> SchematronPhase {
930    unsafe {
931        let id = get_attr(node, "id").unwrap_or_default();
932        let mut phase = SchematronPhase {
933            id,
934            active_patterns: Vec::new(),
935        };
936
937        let mut child = (*node).children;
938        while !child.is_null() {
939            if (*child).type_ == XML_ELEMENT_NODE as c_int {
940                let local = get_local_name(child);
941                if local == "active" {
942                    if let Some(pattern) = get_attr(child, "pattern") {
943                        phase.active_patterns.push(pattern);
944                    }
945                }
946            }
947            child = (*child).next;
948        }
949
950        phase
951    }
952}
953
954/// Parse a `<diagnostics>` element.
955///
956/// # SAFETY
957///
958/// - `node` must be a valid pointer to a `<diagnostics>` element node.
959unsafe fn schematron_parse_diagnostics(node: *mut _xmlNode, schema: &mut SchematronSchema) {
960    unsafe {
961        let mut child = (*node).children;
962        while !child.is_null() {
963            if (*child).type_ == XML_ELEMENT_NODE as c_int {
964                let local = get_local_name(child);
965                if local == "diagnostic" {
966                    let diag = schematron_parse_diagnostic(child);
967                    schema.diagnostics.insert(diag.id.clone(), diag);
968                }
969            }
970            child = (*child).next;
971        }
972    }
973}
974
975/// Parse a `<diagnostic>` element.
976///
977/// # SAFETY
978///
979/// - `node` must be a valid pointer to a `<diagnostic>` element node.
980unsafe fn schematron_parse_diagnostic(node: *mut _xmlNode) -> SchematronDiagnostic {
981    unsafe {
982        let id = get_attr(node, "id").unwrap_or_default();
983        let text = get_inline_text(node);
984        let icon = get_attr(node, "icon");
985        let see = get_attr(node, "see");
986
987        SchematronDiagnostic {
988            id,
989            text,
990            icon,
991            see,
992        }
993    }
994}
995
996/// Parse an `<include>` element (basic support).
997///
998/// # SAFETY
999///
1000/// - `node` must be a valid pointer to an `<include>` element node.
1001unsafe fn schematron_parse_include(node: *mut _xmlNode, _schema: &mut SchematronSchema) {
1002    unsafe {
1003        let href = get_attr(node, "href");
1004        if let Some(url) = href {
1005            let url_c = std::ffi::CString::new(url.clone()).ok();
1006            if let Some(c) = url_c {
1007                let doc = crate::abi::exports_xml2::xmlParseFile(c.as_ptr());
1008                if !doc.is_null() {
1009                    // Find the root element
1010                    let mut root = (*doc).children;
1011                    while !root.is_null() && (*root).type_ != XML_ELEMENT_NODE as c_int {
1012                        root = (*root).next;
1013                    }
1014                    if !root.is_null() {
1015                        let local = get_local_name(root);
1016                        if local == "schema" || local == "pattern" || local == "rule" {
1017                            // In a full implementation, we'd merge the included content
1018                            // For basic support, we just note the include
1019                            // (deeper parsing would require mutable access to schema)
1020                        }
1021                    }
1022                    crate::abi::exports_xml2::xmlFreeDoc(doc);
1023                }
1024            }
1025        }
1026    }
1027}
1028
1029// ═══════════════════════════════════════════════════════════════════════════════
1030// Diagnostic Message Expansion
1031// ═══════════════════════════════════════════════════════════════════════════════
1032
1033/// Expand a diagnostic message, processing `<name/>` and `<value-of/>` placeholders.
1034///
1035/// `<name/>` is replaced with the qualified name of the context node.
1036/// `<value-of select="expr"/>` is replaced with the string value of the XPath expression.
1037///
1038/// # SAFETY
1039///
1040/// - `context_node` must be a valid pointer to an _xmlNode or NULL.
1041unsafe fn expand_diagnostic_message(
1042    text: &str,
1043    context_node: *mut _xmlNode,
1044    xpath_ctxt: &mut XPathContext,
1045) -> String {
1046    // For simplicity, we handle basic patterns.
1047    // In a full implementation, we'd parse the text for <name/> and <value-of/> elements.
1048    // Since the text was extracted from the XML element's inline content,
1049    // we don't have the original markup. We handle this by noting that
1050    // during validation, we generate the message using the pattern's text
1051    // template if it contains placeholders.
1052    //
1053    // For now, we just return the text as-is, since full template expansion
1054    // would require the original element markup.
1055    //
1056    // UPSTREAM-PARITY: libxml2 does minimal message expansion.
1057    let _ = context_node;
1058    let _ = xpath_ctxt;
1059    text.to_string()
1060}
1061
1062// ═══════════════════════════════════════════════════════════════════════════════
1063// Schematron Validation Logic
1064// ═══════════════════════════════════════════════════════════════════════════════
1065
1066/// Check if an XPath expression evaluates to true in the given context.
1067fn evaluate_xpath_boolean(
1068    compiled: &CompiledExpr,
1069    xpath_ctxt: &mut XPathContext,
1070) -> Result<bool, String> {
1071    match crate::xml::xpath::evaluate(compiled, xpath_ctxt) {
1072        Some(value) => Ok(value.as_boolean()),
1073        None => Err("XPath evaluation failed".to_string()),
1074    }
1075}
1076
1077/// Validate an XML document against a Schematron schema.
1078///
1079/// Returns `true` if the document is valid.
1080///
1081/// # SAFETY
1082///
1083/// - `schema` must be a valid reference to a parsed schema.
1084/// - `doc` must be a valid pointer to an _xmlDoc or NULL.
1085/// - `ctxt` must be a valid mutable reference to a validation context.
1086pub unsafe fn schematron_validate_doc(
1087    schema: &SchematronSchema,
1088    doc: *mut _xmlDoc,
1089    ctxt: &mut SchematronValidCtxt,
1090) -> bool {
1091    unsafe {
1092        if doc.is_null() {
1093            ctxt.record_error("Document is null".to_string());
1094            return false;
1095        }
1096
1097        let root = (*doc).children;
1098        if root.is_null() {
1099            ctxt.record_error("Document has no children".to_string());
1100            return false;
1101        }
1102
1103        // Find the root element
1104        let mut root_elem = root;
1105        while !root_elem.is_null() && (*root_elem).type_ != XML_ELEMENT_NODE as c_int {
1106            root_elem = (*root_elem).next;
1107        }
1108
1109        if root_elem.is_null() {
1110            ctxt.record_error("Document has no root element".to_string());
1111            return false;
1112        }
1113
1114        // Get active rules based on phase
1115        let phase_id = ctxt.active_phase.as_deref();
1116        let rules = schema.active_rules(phase_id);
1117
1118        if rules.is_empty() {
1119            // No rules to validate against — consider it valid
1120            return true;
1121        }
1122
1123        // Create XPath context
1124        let mut xpath_ctxt = XPathContext::new(doc);
1125
1126        // Register core XPath functions (true, false, not, string, number, etc.)
1127        let core_funcs = crate::xml::xpath::functions::core_functions();
1128        for (name, func) in core_funcs {
1129            xpath_ctxt.register_function(&name, func);
1130        }
1131
1132        // Register namespace prefixes
1133        for (prefix, uri) in &schema.ns {
1134            xpath_ctxt.namespaces.insert(prefix.clone(), uri.clone());
1135        }
1136
1137        let mut valid = true;
1138
1139        // For each rule, find matching nodes and evaluate patterns
1140        for rule in &rules {
1141            // Find nodes matching the rule's context expression
1142            let matching_nodes: Vec<*mut _xmlNode> =
1143                find_matching_nodes(rule, root_elem, doc, &mut xpath_ctxt);
1144
1145            for context_node in &matching_nodes {
1146                // Set the context node
1147                xpath_ctxt.set_context_node(*context_node);
1148
1149                // Evaluate each pattern (assert/report) in the rule
1150                for pattern in &rule.patterns {
1151                    let compiled = match &pattern.compiled_test {
1152                        Some(c) => c,
1153                        None => continue,
1154                    };
1155
1156                    let test_result = match evaluate_xpath_boolean(compiled, &mut xpath_ctxt) {
1157                        Ok(val) => val,
1158                        Err(e) => {
1159                            ctxt.record_error(format!(
1160                                "XPath error in '{}' test '{}': {}",
1161                                if pattern.pattern_type == SchematronPatternType::Assert {
1162                                    "assert"
1163                                } else {
1164                                    "report"
1165                                },
1166                                pattern.test,
1167                                e
1168                            ));
1169                            valid = false;
1170                            continue;
1171                        }
1172                    };
1173
1174                    let message =
1175                        expand_diagnostic_message(&pattern.text, *context_node, &mut xpath_ctxt);
1176
1177                    match pattern.pattern_type {
1178                        SchematronPatternType::Assert => {
1179                            // Assert: test must be true; if false, it's an error
1180                            if !test_result {
1181                                let node_name = get_node_qname(*context_node);
1182                                let flag_str = pattern
1183                                    .flag
1184                                    .as_ref()
1185                                    .map(|f| format!(" [{}]", f))
1186                                    .unwrap_or_default();
1187                                let role_str = pattern
1188                                    .role
1189                                    .as_ref()
1190                                    .map(|r| format!(" ({})", r))
1191                                    .unwrap_or_default();
1192                                let msg = if message.is_empty() {
1193                                    format!(
1194                                        "assertion failed: '{}' for node '{}'{}{}",
1195                                        pattern.test, node_name, flag_str, role_str
1196                                    )
1197                                } else {
1198                                    format!(
1199                                        "assertion '{}' failed for node '{}'{}{}: {}",
1200                                        pattern.test, node_name, flag_str, role_str, message
1201                                    )
1202                                };
1203                                ctxt.record_error(msg);
1204                                valid = false;
1205                            }
1206                        }
1207                        SchematronPatternType::Report => {
1208                            // Report: test must be false; if true, it's an error
1209                            if test_result {
1210                                let node_name = get_node_qname(*context_node);
1211                                let flag_str = pattern
1212                                    .flag
1213                                    .as_ref()
1214                                    .map(|f| format!(" [{}]", f))
1215                                    .unwrap_or_default();
1216                                let role_str = pattern
1217                                    .role
1218                                    .as_ref()
1219                                    .map(|r| format!(" ({})", r))
1220                                    .unwrap_or_default();
1221                                let msg = if message.is_empty() {
1222                                    format!(
1223                                        "report triggered: '{}' for node '{}'{}{}",
1224                                        pattern.test, node_name, flag_str, role_str
1225                                    )
1226                                } else {
1227                                    format!(
1228                                        "report '{}' triggered for node '{}'{}{}: {}",
1229                                        pattern.test, node_name, flag_str, role_str, message
1230                                    )
1231                                };
1232                                ctxt.record_error(msg);
1233                                valid = false;
1234                            }
1235                        }
1236                    }
1237                }
1238            }
1239        }
1240
1241        valid
1242    }
1243}
1244
1245/// Find nodes matching a rule's context XPath expression.
1246///
1247/// # SAFETY
1248///
1249/// - `root` must be a valid pointer to an element node.
1250/// - `doc` must be a valid pointer to an _xmlDoc.
1251unsafe fn find_matching_nodes(
1252    rule: &SchematronRule,
1253    root: *mut _xmlNode,
1254    doc: *mut _xmlDoc,
1255    xpath_ctxt: &mut XPathContext,
1256) -> Vec<*mut _xmlNode> {
1257    unsafe {
1258        // If the rule has no context, it matches all elements
1259        if rule.context.is_empty() {
1260            let mut nodes = Vec::new();
1261            collect_all_elements(root, &mut nodes);
1262            return nodes;
1263        }
1264
1265        // Try to evaluate the context as an XPath expression
1266        if let Some(compiled) = &rule.compiled_context {
1267            // For simple element names (no XPath special chars), prefer simple matching
1268            // because compiled 'root' means child::root (children named root), not root itself.
1269            let is_simple_name = !rule.context.contains('/')
1270                && !rule.context.contains("::")
1271                && !rule.context.contains('[')
1272                && !rule.context.contains('(');
1273
1274            if !is_simple_name {
1275                xpath_ctxt.set_context_node(root);
1276                xpath_ctxt.document = doc;
1277
1278                match crate::xml::xpath::evaluate(compiled, xpath_ctxt) {
1279                    Some(XPathValue::NodeSet(ns)) => {
1280                        if !ns.is_empty() {
1281                            return ns.iter().collect();
1282                        }
1283                    }
1284                    _ => {}
1285                }
1286            }
1287
1288            // Fall back to simple context matching
1289            simple_context_match(&rule.context, root)
1290        } else {
1291            // No compiled expression — try simple context matching
1292            simple_context_match(&rule.context, root)
1293        }
1294    }
1295}
1296
1297/// Simple context matching for XPath-like context expressions.
1298/// This is a fallback when XPath compilation fails.
1299fn simple_context_match(context: &str, root: *mut _xmlNode) -> Vec<*mut _xmlNode> {
1300    unsafe {
1301        let context = context.trim();
1302
1303        // Handle simple cases:
1304        // "*" — match all elements
1305        // "//element" — match all elements with that name
1306        // "element" — match direct child elements with that name
1307        // "parent/element" — match nested elements
1308
1309        if context == "*" || context == "//*" {
1310            let mut nodes = Vec::new();
1311            collect_all_elements(root, &mut nodes);
1312            return nodes;
1313        }
1314
1315        if context.starts_with("//") {
1316            let name = &context[2..];
1317            if name.is_empty() || name == "*" {
1318                let mut nodes = Vec::new();
1319                collect_all_elements(root, &mut nodes);
1320                return nodes;
1321            }
1322            // Match all elements with the given name anywhere
1323            let mut nodes = Vec::new();
1324            collect_elements_by_name(root, name, &mut nodes);
1325            return nodes;
1326        }
1327
1328        if !context.contains('/') && !context.contains("::") {
1329            // Simple element name — match both the root element and its children
1330            let mut nodes = Vec::new();
1331            // Check if the root element itself matches the context
1332            let root_qname = get_node_qname(root);
1333            let root_local = get_local_name(root);
1334            if root_qname == context || root_local == context || context == "*" {
1335                nodes.push(root);
1336            }
1337            // Also check children
1338            let mut child = (*root).children;
1339            while !child.is_null() {
1340                if (*child).type_ == XML_ELEMENT_NODE as c_int {
1341                    let qname = get_node_qname(child);
1342                    let local = get_local_name(child);
1343                    if qname == context || local == context || context == "*" {
1344                        nodes.push(child);
1345                    }
1346                }
1347                child = (*child).next;
1348            }
1349            return nodes;
1350        }
1351
1352        // For more complex contexts, we just return the root
1353        vec![root]
1354    }
1355}
1356
1357/// Collect all element nodes recursively.
1358///
1359/// # SAFETY
1360///
1361/// - `node` must be a valid pointer to an _xmlNode or NULL.
1362unsafe fn collect_all_elements(node: *mut _xmlNode, nodes: &mut Vec<*mut _xmlNode>) {
1363    unsafe {
1364        if node.is_null() {
1365            return;
1366        }
1367        if (*node).type_ == XML_ELEMENT_NODE as c_int {
1368            nodes.push(node);
1369        }
1370        let mut child = (*node).children;
1371        while !child.is_null() {
1372            collect_all_elements(child, nodes);
1373            child = (*child).next;
1374        }
1375    }
1376}
1377
1378/// Collect elements with a specific name recursively.
1379///
1380/// # SAFETY
1381///
1382/// - `node` must be a valid pointer to an _xmlNode or NULL.
1383unsafe fn collect_elements_by_name(
1384    node: *mut _xmlNode,
1385    name: &str,
1386    nodes: &mut Vec<*mut _xmlNode>,
1387) {
1388    unsafe {
1389        if node.is_null() {
1390            return;
1391        }
1392        if (*node).type_ == XML_ELEMENT_NODE as c_int {
1393            let qname = get_node_qname(node);
1394            let local = get_local_name(node);
1395            if qname == name || local == name {
1396                nodes.push(node);
1397            }
1398        }
1399        let mut child = (*node).children;
1400        while !child.is_null() {
1401            collect_elements_by_name(child, name, nodes);
1402            child = (*child).next;
1403        }
1404    }
1405}
1406
1407// ═══════════════════════════════════════════════════════════════════════════════
1408// Public API Functions
1409// ═══════════════════════════════════════════════════════════════════════════════
1410
1411/// Parse a Schematron schema from an XML string.
1412///
1413/// Returns the parsed schema, or an error message on failure.
1414pub fn schematron_parse_schema(xml_doc: &str) -> Result<SchematronSchema, String> {
1415    schematron_parse(xml_doc)
1416}
1417
1418/// Parse a Schematron schema from a parsed XML document.
1419///
1420/// # SAFETY
1421///
1422/// - `doc` must be a valid pointer to an _xmlDoc.
1423pub unsafe fn schematron_parse_schema_doc(doc: *mut _xmlDoc) -> Result<SchematronSchema, String> {
1424    schematron_parse_doc(doc)
1425}
1426
1427/// Validate a document against a Schematron schema.
1428///
1429/// Returns `true` if the document is valid.
1430///
1431/// # SAFETY
1432///
1433/// - `doc` must be a valid pointer to an _xmlDoc or NULL.
1434pub unsafe fn schematron_validate_doc_schema(
1435    schema: &SchematronSchema,
1436    doc: *mut _xmlDoc,
1437    ctxt: &mut SchematronValidCtxt,
1438) -> bool {
1439    schematron_validate_doc(schema, doc, ctxt)
1440}
1441
1442// ═══════════════════════════════════════════════════════════════════════════════
1443// C ABI Functions
1444// ═══════════════════════════════════════════════════════════════════════════════
1445
1446// These are the C-compatible entry points that get exported via the ABI layer.
1447// They use raw pointers and follow libxml2's calling conventions.
1448
1449/// Create a new Schematron parser context.
1450///
1451/// # UPSTREAM-PARITY
1452///
1453/// ```c
1454/// xmlSchematronParserCtxtPtr xmlSchematronNewParserCtxt(const char *URL);
1455/// ```
1456///
1457/// # SAFETY
1458///
1459/// - `url` must be a valid null-terminated C string or NULL.
1460#[no_mangle]
1461pub unsafe extern "C" fn xmlSchematronNewParserCtxt(url: *const c_char) -> *mut c_void {
1462    if url.is_null() {
1463        // UPSTREAM-PARITY: a NULL URL yields an empty parser context that
1464        // later accepts xmlSchematronParse. The context must be a real
1465        // constructed SchematronSchema (Box) — zeroed raw memory would be
1466        // re-interpreted as a Rust struct with Vec/HashMap fields and
1467        // cloned/dropped later (UB).
1468        return Box::into_raw(Box::new(SchematronSchema::new())) as *mut c_void;
1469    }
1470
1471    let url_str = unsafe {
1472        let mut len = 0;
1473        while *url.add(len) != 0 {
1474            len += 1;
1475        }
1476        let slice = std::slice::from_raw_parts(url as *const u8, len);
1477        String::from_utf8_lossy(slice).to_string()
1478    };
1479
1480    // Try to parse the schema from the URL
1481    if !url_str.is_empty() {
1482        let url_c = std::ffi::CString::new(url_str.clone()).ok();
1483        if let Some(c) = url_c {
1484            let doc = crate::abi::exports_xml2::xmlParseFile(c.as_ptr());
1485            if !doc.is_null() {
1486                let result = schematron_parse_doc(doc);
1487                crate::abi::exports_xml2::xmlFreeDoc(doc);
1488                if let Ok(schema) = result {
1489                    let schema_box = Box::new(schema);
1490                    return Box::into_raw(schema_box) as *mut c_void;
1491                }
1492            }
1493        }
1494    }
1495
1496    // Return empty context for later parsing
1497    Box::into_raw(Box::new(SchematronSchema::new())) as *mut c_void
1498}
1499
1500/// Create a new Schematron parser context from a memory buffer.
1501///
1502/// # UPSTREAM-PARITY
1503///
1504/// ```c
1505/// xmlSchematronParserCtxtPtr xmlSchematronNewMemParserCtxt(const char *buffer, int size);
1506/// ```
1507///
1508/// # SAFETY
1509///
1510/// - `buffer` must be a valid pointer to a buffer of at least `size` bytes.
1511#[no_mangle]
1512pub unsafe extern "C" fn xmlSchematronNewMemParserCtxt(
1513    buffer: *const c_char,
1514    size: c_int,
1515) -> *mut c_void {
1516    if buffer.is_null() || size <= 0 {
1517        return ptr::null_mut();
1518    }
1519
1520    // Parse the schema immediately
1521    let buf_slice = unsafe { std::slice::from_raw_parts(buffer as *const u8, size as usize) };
1522    let xml_str = String::from_utf8_lossy(buf_slice).to_string();
1523
1524    match schematron_parse(&xml_str) {
1525        Ok(schema) => {
1526            let schema_box = Box::new(schema);
1527            Box::into_raw(schema_box) as *mut c_void
1528        }
1529        Err(_) => ptr::null_mut(),
1530    }
1531}
1532
1533/// Parse a Schematron schema.
1534///
1535/// # UPSTREAM-PARITY
1536///
1537/// ```c
1538/// xmlSchematronPtr xmlSchematronParse(xmlSchematronParserCtxtPtr ctxt);
1539/// ```
1540///
1541/// # SAFETY
1542///
1543/// - `ctxt` must be a valid pointer to a parser context, or NULL.
1544#[no_mangle]
1545pub unsafe extern "C" fn xmlSchematronParse(ctxt: *mut c_void) -> *mut c_void {
1546    if ctxt.is_null() {
1547        return ptr::null_mut();
1548    }
1549
1550    // If the context already contains a parsed schema (from xmlSchematronNewMemParserCtxt),
1551    // return it. Otherwise, return the context as-is.
1552    ctxt
1553}
1554
1555/// Free a Schematron schema.
1556///
1557/// # UPSTREAM-PARITY
1558///
1559/// ```c
1560/// void xmlSchematronFree(xmlSchematronPtr schema);
1561/// ```
1562///
1563/// # SAFETY
1564///
1565/// - `schema` must be a valid pointer to a schema, or NULL.
1566#[no_mangle]
1567pub unsafe extern "C" fn xmlSchematronFree(schema: *mut c_void) {
1568    if schema.is_null() {
1569        return;
1570    }
1571    // SAFETY: Reconstruct the Box to drop it.
1572    unsafe {
1573        let _ = Box::from_raw(schema as *mut SchematronSchema);
1574    }
1575}
1576
1577/// Free a Schematron parser context.
1578///
1579/// # UPSTREAM-PARITY
1580///
1581/// ```c
1582/// void xmlSchematronFreeParserCtxt(xmlSchematronParserCtxtPtr ctxt);
1583/// ```
1584///
1585/// # SAFETY
1586///
1587/// - `ctxt` must be a valid pointer to a parser context, or NULL.
1588#[no_mangle]
1589pub unsafe extern "C" fn xmlSchematronFreeParserCtxt(ctxt: *mut c_void) {
1590    if ctxt.is_null() {
1591        return;
1592    }
1593    // SAFETY: Reconstruct the Box to drop it.
1594    unsafe {
1595        let _ = Box::from_raw(ctxt as *mut SchematronSchema);
1596    }
1597}
1598
1599/// Create a new Schematron validation context.
1600///
1601/// # UPSTREAM-PARITY
1602///
1603/// ```c
1604/// xmlSchematronValidCtxtPtr xmlSchematronNewValidCtxt(xmlSchematronPtr schema);
1605/// ```
1606///
1607/// # SAFETY
1608///
1609/// - `schema` must be a valid pointer to a schema, or NULL.
1610#[no_mangle]
1611pub unsafe extern "C" fn xmlSchematronNewValidCtxt(schema: *mut c_void) -> *mut c_void {
1612    let mut ctxt = SchematronValidCtxt::new();
1613
1614    if !schema.is_null() {
1615        // SAFETY: The schema pointer is assumed to be a valid SchematronSchema.
1616        unsafe {
1617            let schema_ref = &*(schema as *const SchematronSchema);
1618            ctxt.schema = Some(schema_ref.clone());
1619        }
1620    }
1621
1622    let boxed = Box::new(ctxt);
1623    Box::into_raw(boxed) as *mut c_void
1624}
1625
1626/// Free a Schematron validation context.
1627///
1628/// # UPSTREAM-PARITY
1629///
1630/// ```c
1631/// void xmlSchematronFreeValidCtxt(xmlSchematronValidCtxtPtr ctxt);
1632/// ```
1633///
1634/// # SAFETY
1635///
1636/// - `ctxt` must be a valid pointer to a validation context, or NULL.
1637#[no_mangle]
1638pub unsafe extern "C" fn xmlSchematronFreeValidCtxt(ctxt: *mut c_void) {
1639    if ctxt.is_null() {
1640        return;
1641    }
1642    // SAFETY: Reconstruct the Box to drop it.
1643    unsafe {
1644        let _ = Box::from_raw(ctxt as *mut SchematronValidCtxt);
1645    }
1646}
1647
1648/// Validate a document against a Schematron schema.
1649///
1650/// # UPSTREAM-PARITY
1651///
1652/// ```c
1653/// int xmlSchematronValidateDoc(xmlSchematronValidCtxtPtr ctxt, xmlDocPtr doc);
1654/// ```
1655///
1656/// Returns 0 if valid, -1 on internal error, or the number of validation errors.
1657///
1658/// # SAFETY
1659///
1660/// - `ctxt` must be a valid pointer to a validation context, or NULL.
1661/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
1662#[no_mangle]
1663pub unsafe extern "C" fn xmlSchematronValidateDoc(ctxt: *mut c_void, doc: *mut _xmlDoc) -> c_int {
1664    if ctxt.is_null() || doc.is_null() {
1665        return -1;
1666    }
1667
1668    unsafe {
1669        let valid_ctxt = &mut *(ctxt as *mut SchematronValidCtxt);
1670        let schema = match &valid_ctxt.schema {
1671            Some(s) => s,
1672            None => return -1,
1673        };
1674
1675        let mut temp_ctxt = SchematronValidCtxt::new();
1676        temp_ctxt.active_phase = valid_ctxt.active_phase.clone();
1677
1678        let valid = schematron_validate_doc(schema, doc, &mut temp_ctxt);
1679
1680        if valid {
1681            0
1682        } else {
1683            valid_ctxt.errors = temp_ctxt.errors;
1684            valid_ctxt.nb_errors = temp_ctxt.nb_errors;
1685            temp_ctxt.nb_errors
1686        }
1687    }
1688}
1689
1690// ═══════════════════════════════════════════════════════════════════════════════
1691// Tests
1692// ═══════════════════════════════════════════════════════════════════════════════
1693
1694#[cfg(test)]
1695mod tests {
1696    use super::*;
1697
1698    // ── Schema Parsing Tests ──────────────────────────────────────────────
1699
1700    #[test]
1701    fn test_parse_simple_schema() {
1702        let schema_xml = r#"<?xml version="1.0"?>
1703<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1704  <pattern id="P1">
1705    <rule context="root">
1706      <assert test="count(*) > 0">Root must have children</assert>
1707    </rule>
1708  </pattern>
1709</schema>"#;
1710
1711        let result = schematron_parse(schema_xml);
1712        assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
1713        let schema = result.unwrap();
1714        assert_eq!(schema.pattern_order.len(), 1);
1715        assert_eq!(schema.rules.len(), 1);
1716    }
1717
1718    #[test]
1719    fn test_parse_with_ns() {
1720        let schema_xml = r#"<?xml version="1.0"?>
1721<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1722  <ns prefix="doc" uri="http://example.com/doc"/>
1723  <pattern id="P1">
1724    <rule context="doc:entry">
1725      <assert test="doc:title">Entry must have a title</assert>
1726    </rule>
1727  </pattern>
1728</schema>"#;
1729
1730        let result = schematron_parse(schema_xml);
1731        assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
1732        let schema = result.unwrap();
1733        assert!(schema.ns.contains_key("doc"));
1734        assert_eq!(schema.ns.get("doc").unwrap(), "http://example.com/doc");
1735    }
1736
1737    #[test]
1738    fn test_parse_with_phases() {
1739        let schema_xml = r#"<?xml version="1.0"?>
1740<schema xmlns="http://purl.oclc.org/dsdl/schematron" defaultPhase="phaseA">
1741  <phase id="phaseA">
1742    <active pattern="P1"/>
1743  </phase>
1744  <phase id="phaseB">
1745    <active pattern="P2"/>
1746  </phase>
1747  <pattern id="P1">
1748    <rule context="root">
1749      <assert test="true()">Always passes</assert>
1750    </rule>
1751  </pattern>
1752  <pattern id="P2">
1753    <rule context="root">
1754      <assert test="false()">Always fails</assert>
1755    </rule>
1756  </pattern>
1757</schema>"#;
1758
1759        let result = schematron_parse(schema_xml);
1760        assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
1761        let schema = result.unwrap();
1762        assert_eq!(schema.phases.len(), 2);
1763        assert!(schema.phases.contains_key("phaseA"));
1764        assert!(schema.phases.contains_key("phaseB"));
1765        assert_eq!(schema.default_phase.as_deref(), Some("phaseA"));
1766    }
1767
1768    #[test]
1769    fn test_parse_report_pattern() {
1770        let schema_xml = r#"<?xml version="1.0"?>
1771<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1772  <pattern id="P1">
1773    <rule context="root">
1774      <report test="@deprecated">Element is deprecated</report>
1775    </rule>
1776  </pattern>
1777</schema>"#;
1778
1779        let result = schematron_parse(schema_xml);
1780        assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
1781        let schema = result.unwrap();
1782        let rule = schema.rules.values().next().unwrap();
1783        assert_eq!(rule.patterns.len(), 1);
1784        assert_eq!(rule.patterns[0].pattern_type, SchematronPatternType::Report);
1785        assert_eq!(rule.patterns[0].test, "@deprecated");
1786    }
1787
1788    #[test]
1789    fn test_parse_abstract_rule() {
1790        let schema_xml = r#"<?xml version="1.0"?>
1791<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1792  <pattern id="P1">
1793    <rule id="abstractRule" abstract="true" context="*">
1794      <assert test="true()">Abstract assertion</assert>
1795    </rule>
1796    <rule id="concreteRule" context="root">
1797      <extends rule="abstractRule"/>
1798      <assert test="count(*) > 0">Concrete assertion</assert>
1799    </rule>
1800  </pattern>
1801</schema>"#;
1802
1803        let result = schematron_parse(schema_xml);
1804        assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
1805        let schema = result.unwrap();
1806        assert!(schema.rules.contains_key("abstractRule"));
1807        assert!(schema.rules.contains_key("concreteRule"));
1808        let abstract_rule = &schema.rules["abstractRule"];
1809        assert!(abstract_rule.abstract_);
1810        let concrete_rule = &schema.rules["concreteRule"];
1811        assert!(!concrete_rule.abstract_);
1812        assert_eq!(concrete_rule.extends.len(), 1);
1813        assert_eq!(concrete_rule.extends[0], "abstractRule");
1814    }
1815
1816    #[test]
1817    fn test_parse_with_diagnostics() {
1818        let schema_xml = r#"<?xml version="1.0"?>
1819<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1820  <diagnostics>
1821    <diagnostic id="diag1">This is a diagnostic message</diagnostic>
1822  </diagnostics>
1823  <pattern id="P1">
1824    <rule context="root">
1825      <assert test="true()" diagnostics="diag1">Assertion with diagnostic</assert>
1826    </rule>
1827  </pattern>
1828</schema>"#;
1829
1830        let result = schematron_parse(schema_xml);
1831        assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
1832        let schema = result.unwrap();
1833        assert!(schema.diagnostics.contains_key("diag1"));
1834        assert_eq!(
1835            schema.diagnostics["diag1"].text,
1836            "This is a diagnostic message"
1837        );
1838    }
1839
1840    #[test]
1841    fn test_parse_with_attributes() {
1842        let schema_xml = r#"<?xml version="1.0"?>
1843<schema xmlns="http://purl.oclc.org/dsdl/schematron" title="Test Schema">
1844  <pattern id="P1">
1845    <rule context="root">
1846      <assert test="true()" flag="warn" role="error" id="a1" icon="info" see="http://example.com">
1847        Test message
1848      </assert>
1849    </rule>
1850  </pattern>
1851</schema>"#;
1852
1853        let result = schematron_parse(schema_xml);
1854        assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
1855        let schema = result.unwrap();
1856        assert_eq!(schema.title.as_deref(), Some("Test Schema"));
1857        let rule = schema.rules.values().next().unwrap();
1858        let pat = &rule.patterns[0];
1859        assert_eq!(pat.flag.as_deref(), Some("warn"));
1860        assert_eq!(pat.role.as_deref(), Some("error"));
1861        assert_eq!(pat.id.as_deref(), Some("a1"));
1862        assert_eq!(pat.icon.as_deref(), Some("info"));
1863        assert_eq!(pat.see.as_deref(), Some("http://example.com"));
1864    }
1865
1866    #[test]
1867    fn test_parse_empty_schema() {
1868        let schema_xml = r#"<?xml version="1.0"?>
1869<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1870</schema>"#;
1871
1872        let result = schematron_parse(schema_xml);
1873        assert!(result.is_ok(), "Failed to parse empty schema");
1874        let schema = result.unwrap();
1875        assert!(schema.rules.is_empty());
1876        assert!(schema.phases.is_empty());
1877    }
1878
1879    #[test]
1880    fn test_parse_no_assertions() {
1881        let schema_xml = r#"<?xml version="1.0"?>
1882<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1883  <pattern id="P1">
1884    <rule context="root">
1885    </rule>
1886  </pattern>
1887</schema>"#;
1888
1889        let result = schematron_parse(schema_xml);
1890        assert!(result.is_ok(), "Failed to parse schema with no assertions");
1891        let schema = result.unwrap();
1892        let rule = schema.rules.values().next().unwrap();
1893        assert!(rule.patterns.is_empty());
1894    }
1895
1896    #[test]
1897    fn test_parse_invalid_root_element() {
1898        let schema_xml = r#"<?xml version="1.0"?>
1899<not-schema xmlns="http://purl.oclc.org/dsdl/schematron">
1900</not-schema>"#;
1901
1902        let result = schematron_parse(schema_xml);
1903        assert!(result.is_err(), "Should fail with wrong root element");
1904        assert!(
1905            result.err().unwrap().contains("Expected '<schema>'"),
1906            "Error should mention expected schema element"
1907        );
1908    }
1909
1910    #[test]
1911    fn test_parse_empty_document_fails() {
1912        let result = schematron_parse("");
1913        assert!(result.is_err());
1914    }
1915
1916    #[test]
1917    fn test_parse_invalid_xml_fails() {
1918        let result = schematron_parse("not valid xml <<<");
1919        assert!(result.is_err());
1920    }
1921
1922    #[test]
1923    fn test_parse_schema_with_let_and_param() {
1924        let schema_xml = r#"<?xml version="1.0"?>
1925<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1926  <pattern id="P1">
1927    <rule context="root">
1928      <let name="x" value="42"/>
1929      <param name="debug" value="true"/>
1930      <assert test="true()">Test with let and param</assert>
1931    </rule>
1932  </pattern>
1933</schema>"#;
1934
1935        let result = schematron_parse(schema_xml);
1936        assert!(
1937            result.is_ok(),
1938            "Failed to parse schema with let/param: {:?}",
1939            result.err()
1940        );
1941        let schema = result.unwrap();
1942        assert_eq!(schema.rules.len(), 1);
1943    }
1944
1945    #[test]
1946    fn test_parse_schema_with_documentation() {
1947        let schema_xml = r#"<?xml version="1.0"?>
1948<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1949  <p>This is documentation</p>
1950  <caption>Table caption</caption>
1951  <pattern id="P1">
1952    <p>Pattern documentation</p>
1953    <rule context="root">
1954      <p>Rule documentation</p>
1955      <assert test="true()">Real assertion</assert>
1956    </rule>
1957  </pattern>
1958</schema>"#;
1959
1960        let result = schematron_parse(schema_xml);
1961        assert!(result.is_ok(), "Failed to parse schema with documentation");
1962        let schema = result.unwrap();
1963        assert_eq!(schema.rules.len(), 1);
1964    }
1965
1966    // ── Validation Tests ──────────────────────────────────────────────────
1967
1968    #[test]
1969    fn test_validate_assert_pass() {
1970        let schema_xml = r#"<?xml version="1.0"?>
1971<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1972  <pattern id="P1">
1973    <rule context="root">
1974      <assert test="true()">Always passes</assert>
1975    </rule>
1976  </pattern>
1977</schema>"#;
1978
1979        let doc_xml = r#"<?xml version="1.0"?>
1980<root>Hello</root>"#;
1981
1982        let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
1983
1984        let doc = unsafe {
1985            crate::abi::exports_xml2::xmlReadMemory(
1986                doc_xml.as_ptr() as *const c_char,
1987                doc_xml.len() as c_int,
1988                b"test.xml\0".as_ptr() as *const c_char,
1989                ptr::null(),
1990                0,
1991            )
1992        };
1993        assert!(!doc.is_null(), "Failed to parse document");
1994
1995        let mut ctxt = SchematronValidCtxt::new();
1996        let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
1997        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
1998
1999        assert!(valid, "Validation failed: {:?}", ctxt.errors);
2000    }
2001
2002    #[test]
2003    fn test_validate_assert_fail() {
2004        let schema_xml = r#"<?xml version="1.0"?>
2005<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2006  <pattern id="P1">
2007    <rule context="root">
2008      <assert test="false()">Always fails</assert>
2009    </rule>
2010  </pattern>
2011</schema>"#;
2012
2013        let doc_xml = r#"<?xml version="1.0"?>
2014<root>Hello</root>"#;
2015
2016        let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2017
2018        let doc = unsafe {
2019            crate::abi::exports_xml2::xmlReadMemory(
2020                doc_xml.as_ptr() as *const c_char,
2021                doc_xml.len() as c_int,
2022                b"test.xml\0".as_ptr() as *const c_char,
2023                ptr::null(),
2024                0,
2025            )
2026        };
2027        assert!(!doc.is_null());
2028
2029        let mut ctxt = SchematronValidCtxt::new();
2030        let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2031        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2032
2033        assert!(
2034            !valid,
2035            "Validation should have failed, errors: {:?}",
2036            ctxt.errors
2037        );
2038        assert!(ctxt.nb_errors > 0);
2039    }
2040
2041    #[test]
2042    fn test_validate_report_pass() {
2043        let schema_xml = r#"<?xml version="1.0"?>
2044<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2045  <pattern id="P1">
2046    <rule context="root">
2047      <report test="false()">Report should not trigger</report>
2048    </rule>
2049  </pattern>
2050</schema>"#;
2051
2052        let doc_xml = r#"<?xml version="1.0"?>
2053<root>Hello</root>"#;
2054
2055        let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2056
2057        let doc = unsafe {
2058            crate::abi::exports_xml2::xmlReadMemory(
2059                doc_xml.as_ptr() as *const c_char,
2060                doc_xml.len() as c_int,
2061                b"test.xml\0".as_ptr() as *const c_char,
2062                ptr::null(),
2063                0,
2064            )
2065        };
2066        assert!(!doc.is_null());
2067
2068        let mut ctxt = SchematronValidCtxt::new();
2069        let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2070        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2071
2072        assert!(valid, "Report should not trigger: {:?}", ctxt.errors);
2073    }
2074
2075    #[test]
2076    fn test_validate_report_fail() {
2077        let schema_xml = r#"<?xml version="1.0"?>
2078<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2079  <pattern id="P1">
2080    <rule context="root">
2081      <report test="true()">Report should trigger</report>
2082    </rule>
2083  </pattern>
2084</schema>"#;
2085
2086        let doc_xml = r#"<?xml version="1.0"?>
2087<root>Hello</root>"#;
2088
2089        let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2090
2091        let doc = unsafe {
2092            crate::abi::exports_xml2::xmlReadMemory(
2093                doc_xml.as_ptr() as *const c_char,
2094                doc_xml.len() as c_int,
2095                b"test.xml\0".as_ptr() as *const c_char,
2096                ptr::null(),
2097                0,
2098            )
2099        };
2100        assert!(!doc.is_null());
2101
2102        let mut ctxt = SchematronValidCtxt::new();
2103        let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2104        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2105
2106        assert!(!valid, "Report should have triggered");
2107        assert!(ctxt.nb_errors > 0);
2108    }
2109
2110    #[test]
2111    fn test_validate_context_matching() {
2112        let schema_xml = r#"<?xml version="1.0"?>
2113<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2114  <pattern id="P1">
2115    <rule context="child">
2116      <assert test="true()">Child matches</assert>
2117    </rule>
2118  </pattern>
2119</schema>"#;
2120
2121        let doc_xml = r#"<?xml version="1.0"?>
2122<root>
2123  <child>A</child>
2124  <child>B</child>
2125</root>"#;
2126
2127        let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2128
2129        let doc = unsafe {
2130            crate::abi::exports_xml2::xmlReadMemory(
2131                doc_xml.as_ptr() as *const c_char,
2132                doc_xml.len() as c_int,
2133                b"test.xml\0".as_ptr() as *const c_char,
2134                ptr::null(),
2135                0,
2136            )
2137        };
2138        assert!(!doc.is_null());
2139
2140        let mut ctxt = SchematronValidCtxt::new();
2141        let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2142        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2143
2144        assert!(valid, "Context matching failed: {:?}", ctxt.errors);
2145    }
2146
2147    #[test]
2148    fn test_validate_multiple_rules() {
2149        let schema_xml = r#"<?xml version="1.0"?>
2150<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2151  <pattern id="P1">
2152    <rule context="root">
2153      <assert test="true()">Root passes</assert>
2154    </rule>
2155  </pattern>
2156  <pattern id="P2">
2157    <rule context="child">
2158      <assert test="true()">Child passes</assert>
2159    </rule>
2160  </pattern>
2161</schema>"#;
2162
2163        let doc_xml = r#"<?xml version="1.0"?>
2164<root>
2165  <child>Content</child>
2166</root>"#;
2167
2168        let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2169
2170        let doc = unsafe {
2171            crate::abi::exports_xml2::xmlReadMemory(
2172                doc_xml.as_ptr() as *const c_char,
2173                doc_xml.len() as c_int,
2174                b"test.xml\0".as_ptr() as *const c_char,
2175                ptr::null(),
2176                0,
2177            )
2178        };
2179        assert!(!doc.is_null());
2180
2181        let mut ctxt = SchematronValidCtxt::new();
2182        let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2183        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2184
2185        assert!(valid, "Multiple rules failed: {:?}", ctxt.errors);
2186    }
2187
2188    #[test]
2189    fn test_validate_with_phase_filtering() {
2190        let schema_xml = r#"<?xml version="1.0"?>
2191<schema xmlns="http://purl.oclc.org/dsdl/schematron" defaultPhase="phaseA">
2192  <phase id="phaseA">
2193    <active pattern="P1"/>
2194  </phase>
2195  <phase id="phaseB">
2196    <active pattern="P2"/>
2197  </phase>
2198  <pattern id="P1">
2199    <rule context="root">
2200      <assert test="true()">Always passes</assert>
2201    </rule>
2202  </pattern>
2203  <pattern id="P2">
2204    <rule context="root">
2205      <assert test="false()">Always fails</assert>
2206    </rule>
2207  </pattern>
2208</schema>"#;
2209
2210        let doc_xml = r#"<?xml version="1.0"?>
2211<root>Hello</root>"#;
2212
2213        let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2214
2215        let doc = unsafe {
2216            crate::abi::exports_xml2::xmlReadMemory(
2217                doc_xml.as_ptr() as *const c_char,
2218                doc_xml.len() as c_int,
2219                b"test.xml\0".as_ptr() as *const c_char,
2220                ptr::null(),
2221                0,
2222            )
2223        };
2224        assert!(!doc.is_null());
2225
2226        // Default phase (phaseA) should only include P1 which passes
2227        let mut ctxt = SchematronValidCtxt::new();
2228        let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2229        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2230
2231        assert!(
2232            valid,
2233            "Phase filtering should make validation pass: {:?}",
2234            ctxt.errors
2235        );
2236    }
2237
2238    #[test]
2239    fn test_validate_no_rules() {
2240        let schema_xml = r#"<?xml version="1.0"?>
2241<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2242</schema>"#;
2243
2244        let doc_xml = r#"<?xml version="1.0"?>
2245<root>Hello</root>"#;
2246
2247        let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2248
2249        let doc = unsafe {
2250            crate::abi::exports_xml2::xmlReadMemory(
2251                doc_xml.as_ptr() as *const c_char,
2252                doc_xml.len() as c_int,
2253                b"test.xml\0".as_ptr() as *const c_char,
2254                ptr::null(),
2255                0,
2256            )
2257        };
2258        assert!(!doc.is_null());
2259
2260        let mut ctxt = SchematronValidCtxt::new();
2261        let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2262        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2263
2264        assert!(valid, "Empty schema should pass validation");
2265    }
2266
2267    #[test]
2268    fn test_validate_extends_resolution() {
2269        let schema_xml = r#"<?xml version="1.0"?>
2270<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2271  <pattern id="P1">
2272    <rule id="base" abstract="true" context="*">
2273      <assert test="true()">Base assertion</assert>
2274    </rule>
2275    <rule id="derived" context="root">
2276      <extends rule="base"/>
2277      <assert test="true()">Derived assertion</assert>
2278    </rule>
2279  </pattern>
2280</schema>"#;
2281
2282        let doc_xml = r#"<?xml version="1.0"?>
2283<root>Hello</root>"#;
2284
2285        let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2286
2287        // Test extends resolution
2288        let resolved = schema.resolve_rule("derived");
2289        assert!(resolved.is_some());
2290        let resolved = resolved.unwrap();
2291        // The resolved rule should have patterns from both base and derived
2292        assert_eq!(
2293            resolved.patterns.len(),
2294            2,
2295            "Should have inherited the base pattern"
2296        );
2297
2298        let doc = unsafe {
2299            crate::abi::exports_xml2::xmlReadMemory(
2300                doc_xml.as_ptr() as *const c_char,
2301                doc_xml.len() as c_int,
2302                b"test.xml\0".as_ptr() as *const c_char,
2303                ptr::null(),
2304                0,
2305            )
2306        };
2307        assert!(!doc.is_null());
2308
2309        let mut ctxt = SchematronValidCtxt::new();
2310        let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2311        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2312
2313        assert!(valid, "Extends resolution failed: {:?}", ctxt.errors);
2314    }
2315
2316    #[test]
2317    fn test_validate_assert_with_flag() {
2318        let schema_xml = r#"<?xml version="1.0"?>
2319<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2320  <pattern id="P1">
2321    <rule context="root">
2322      <assert test="false()" flag="warn">Warning message</assert>
2323    </rule>
2324  </pattern>
2325</schema>"#;
2326
2327        let doc_xml = r#"<?xml version="1.0"?>
2328<root>Hello</root>"#;
2329
2330        let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2331
2332        let doc = unsafe {
2333            crate::abi::exports_xml2::xmlReadMemory(
2334                doc_xml.as_ptr() as *const c_char,
2335                doc_xml.len() as c_int,
2336                b"test.xml\0".as_ptr() as *const c_char,
2337                ptr::null(),
2338                0,
2339            )
2340        };
2341        assert!(!doc.is_null());
2342
2343        let mut ctxt = SchematronValidCtxt::new();
2344        let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2345        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2346
2347        assert!(!valid);
2348        assert!(ctxt.nb_errors > 0);
2349        // The error message should include the flag
2350        assert!(
2351            ctxt.errors[0].contains("[warn]"),
2352            "Error should include flag"
2353        );
2354    }
2355
2356    // ── C ABI Lifecycle Tests ─────────────────────────────────────────────
2357
2358    #[test]
2359    fn test_c_abi_new_free_parser_ctxt() {
2360        let ctxt = unsafe { xmlSchematronNewParserCtxt(ptr::null()) };
2361        assert!(!ctxt.is_null());
2362        unsafe { xmlSchematronFreeParserCtxt(ctxt) };
2363        // Should not crash
2364    }
2365
2366    #[test]
2367    fn test_c_abi_new_free_valid_ctxt() {
2368        let schema = unsafe { xmlSchematronNewParserCtxt(ptr::null()) };
2369        assert!(!schema.is_null());
2370
2371        let valid_ctxt = unsafe { xmlSchematronNewValidCtxt(schema) };
2372        assert!(!valid_ctxt.is_null());
2373
2374        unsafe { xmlSchematronFreeValidCtxt(valid_ctxt) };
2375        unsafe { xmlSchematronFreeParserCtxt(schema) };
2376        // Should not crash
2377    }
2378
2379    #[test]
2380    fn test_c_abi_parse_free() {
2381        let schema_xml = r#"<?xml version="1.0"?>
2382<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2383  <pattern id="P1">
2384    <rule context="root">
2385      <assert test="true()">Test</assert>
2386    </rule>
2387  </pattern>
2388</schema>"#;
2389
2390        let ctxt = unsafe {
2391            xmlSchematronNewMemParserCtxt(
2392                schema_xml.as_ptr() as *const c_char,
2393                schema_xml.len() as c_int,
2394            )
2395        };
2396        assert!(!ctxt.is_null());
2397
2398        let schema = unsafe { xmlSchematronParse(ctxt) };
2399        assert!(!schema.is_null());
2400
2401        unsafe { xmlSchematronFree(schema) };
2402        // Should not crash
2403    }
2404
2405    #[test]
2406    fn test_c_abi_validate_doc() {
2407        let schema_xml = r#"<?xml version="1.0"?>
2408<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2409  <pattern id="P1">
2410    <rule context="root">
2411      <assert test="true()">Always passes</assert>
2412    </rule>
2413  </pattern>
2414</schema>"#;
2415
2416        let doc_xml = r#"<?xml version="1.0"?>
2417<root>Hello</root>"#;
2418
2419        let ctxt = unsafe {
2420            xmlSchematronNewMemParserCtxt(
2421                schema_xml.as_ptr() as *const c_char,
2422                schema_xml.len() as c_int,
2423            )
2424        };
2425        assert!(!ctxt.is_null());
2426
2427        let schema = unsafe { xmlSchematronParse(ctxt) };
2428        assert!(!schema.is_null());
2429
2430        let valid_ctxt = unsafe { xmlSchematronNewValidCtxt(schema) };
2431        assert!(!valid_ctxt.is_null());
2432
2433        let doc = unsafe {
2434            crate::abi::exports_xml2::xmlReadMemory(
2435                doc_xml.as_ptr() as *const c_char,
2436                doc_xml.len() as c_int,
2437                b"test.xml\0".as_ptr() as *const c_char,
2438                ptr::null(),
2439                0,
2440            )
2441        };
2442        assert!(!doc.is_null());
2443
2444        let result = unsafe { xmlSchematronValidateDoc(valid_ctxt, doc) };
2445        assert_eq!(result, 0, "Validation should pass (return 0)");
2446
2447        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2448        unsafe { xmlSchematronFreeValidCtxt(valid_ctxt) };
2449        unsafe { xmlSchematronFree(schema) };
2450    }
2451
2452    #[test]
2453    fn test_c_abi_validate_fail() {
2454        let schema_xml = r#"<?xml version="1.0"?>
2455<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2456  <pattern id="P1">
2457    <rule context="root">
2458      <assert test="false()">Always fails</assert>
2459    </rule>
2460  </pattern>
2461</schema>"#;
2462
2463        let doc_xml = r#"<?xml version="1.0"?>
2464<root>Hello</root>"#;
2465
2466        let ctxt = unsafe {
2467            xmlSchematronNewMemParserCtxt(
2468                schema_xml.as_ptr() as *const c_char,
2469                schema_xml.len() as c_int,
2470            )
2471        };
2472        assert!(!ctxt.is_null());
2473
2474        let schema = unsafe { xmlSchematronParse(ctxt) };
2475        assert!(!schema.is_null());
2476
2477        let valid_ctxt = unsafe { xmlSchematronNewValidCtxt(schema) };
2478        assert!(!valid_ctxt.is_null());
2479
2480        let doc = unsafe {
2481            crate::abi::exports_xml2::xmlReadMemory(
2482                doc_xml.as_ptr() as *const c_char,
2483                doc_xml.len() as c_int,
2484                b"test.xml\0".as_ptr() as *const c_char,
2485                ptr::null(),
2486                0,
2487            )
2488        };
2489        assert!(!doc.is_null());
2490
2491        let result = unsafe { xmlSchematronValidateDoc(valid_ctxt, doc) };
2492        assert!(result > 0, "Validation should fail (return > 0)");
2493
2494        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2495        unsafe { xmlSchematronFreeValidCtxt(valid_ctxt) };
2496        unsafe { xmlSchematronFree(schema) };
2497    }
2498
2499    #[test]
2500    fn test_c_abi_null_handling() {
2501        // All free functions should handle NULL gracefully
2502        unsafe { xmlSchematronFree(ptr::null_mut()) };
2503        unsafe { xmlSchematronFreeParserCtxt(ptr::null_mut()) };
2504        unsafe { xmlSchematronFreeValidCtxt(ptr::null_mut()) };
2505
2506        // Parse with NULL should return NULL
2507        let result = unsafe { xmlSchematronParse(ptr::null_mut()) };
2508        assert!(result.is_null());
2509
2510        // Validate with NULL should return -1
2511        let result = unsafe { xmlSchematronValidateDoc(ptr::null_mut(), ptr::null_mut()) };
2512        assert_eq!(result, -1);
2513    }
2514
2515    // ── Edge Case Tests ───────────────────────────────────────────────────
2516
2517    #[test]
2518    fn test_validate_null_doc() {
2519        let schema = SchematronSchema::new();
2520        let mut ctxt = SchematronValidCtxt::new();
2521        let valid = unsafe { schematron_validate_doc(&schema, ptr::null_mut(), &mut ctxt) };
2522        assert!(!valid);
2523        assert!(ctxt.nb_errors > 0);
2524    }
2525
2526    #[test]
2527    fn test_active_rules_default_phase() {
2528        let mut schema = SchematronSchema::new();
2529
2530        let rule = SchematronRule::new("root".to_string());
2531        schema.rules.insert("r1".to_string(), rule);
2532
2533        schema
2534            .pattern_groups
2535            .insert("p1".to_string(), vec!["r1".to_string()]);
2536        schema.pattern_order.push("p1".to_string());
2537
2538        let rules = schema.active_rules(None);
2539        assert_eq!(rules.len(), 1);
2540    }
2541
2542    #[test]
2543    fn test_active_rules_unknown_phase() {
2544        let mut schema = SchematronSchema::new();
2545
2546        let rule = SchematronRule::new("root".to_string());
2547        schema.rules.insert("r1".to_string(), rule);
2548
2549        schema
2550            .pattern_groups
2551            .insert("p1".to_string(), vec!["r1".to_string()]);
2552        schema.pattern_order.push("p1".to_string());
2553
2554        let rules = schema.active_rules(Some("nonexistent"));
2555        assert_eq!(rules.len(), 1, "Unknown phase should use all patterns");
2556    }
2557
2558    #[test]
2559    fn test_parse_schema_with_span_and_emph() {
2560        let schema_xml = r#"<?xml version="1.0"?>
2561<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2562  <pattern id="P1">
2563    <rule context="root">
2564      <assert test="true()">Message with <span class="x">inline</span> and <emph>emphasis</emph></assert>
2565    </rule>
2566  </pattern>
2567</schema>"#;
2568
2569        let result = schematron_parse(schema_xml);
2570        assert!(
2571            result.is_ok(),
2572            "Failed to parse schema with span/emph: {:?}",
2573            result.err()
2574        );
2575        let schema = result.unwrap();
2576        let rule = schema.rules.values().next().unwrap();
2577        let pat = &rule.patterns[0];
2578        // The text should include the inline content of span and emph
2579        assert!(
2580            pat.text.contains("inline"),
2581            "Text should include span content"
2582        );
2583        assert!(
2584            pat.text.contains("emphasis"),
2585            "Text should include emph content"
2586        );
2587    }
2588
2589    #[test]
2590    fn test_schematron_pattern_new_assert() {
2591        let pat = SchematronPattern::new(
2592            SchematronPatternType::Assert,
2593            "true()".to_string(),
2594            "Test message".to_string(),
2595        );
2596        assert_eq!(pat.pattern_type, SchematronPatternType::Assert);
2597        assert_eq!(pat.test, "true()");
2598        assert_eq!(pat.text, "Test message");
2599        assert!(pat.compiled_test.is_some());
2600    }
2601
2602    #[test]
2603    fn test_schematron_pattern_new_report() {
2604        let pat = SchematronPattern::new(
2605            SchematronPatternType::Report,
2606            "false()".to_string(),
2607            "Report message".to_string(),
2608        );
2609        assert_eq!(pat.pattern_type, SchematronPatternType::Report);
2610        assert!(pat.compiled_test.is_some());
2611    }
2612
2613    #[test]
2614    fn test_schematron_rule_new() {
2615        let rule = SchematronRule::new("root".to_string());
2616        assert_eq!(rule.context, "root");
2617        assert!(rule.patterns.is_empty());
2618        assert!(!rule.abstract_);
2619    }
2620
2621    #[test]
2622    fn test_schematron_schema_new() {
2623        let schema = SchematronSchema::new();
2624        assert_eq!(schema.query_binding, "xslt");
2625        assert!(schema.rules.is_empty());
2626        assert!(schema.phases.is_empty());
2627        assert!(schema.ns.is_empty());
2628    }
2629
2630    #[test]
2631    fn test_schematron_valid_ctxt_new() {
2632        let ctxt = SchematronValidCtxt::new();
2633        assert!(ctxt.errors.is_empty());
2634        assert_eq!(ctxt.nb_errors, 0);
2635        assert!(ctxt.active_phase.is_none());
2636    }
2637
2638    #[test]
2639    fn test_validate_assert_with_child_count() {
2640        let schema_xml = r#"<?xml version="1.0"?>
2641<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2642  <pattern id="P1">
2643    <rule context="root">
2644      <assert test="count(*) > 0">Root must have at least one child element</assert>
2645    </rule>
2646  </pattern>
2647</schema>"#;
2648
2649        let doc_xml = r#"<?xml version="1.0"?>
2650<root>
2651  <child>Content</child>
2652</root>"#;
2653
2654        let schema = schematron_parse(schema_xml).expect("Failed to parse schema");
2655
2656        let doc = unsafe {
2657            crate::abi::exports_xml2::xmlReadMemory(
2658                doc_xml.as_ptr() as *const c_char,
2659                doc_xml.len() as c_int,
2660                b"test.xml\0".as_ptr() as *const c_char,
2661                ptr::null(),
2662                0,
2663            )
2664        };
2665        assert!(!doc.is_null());
2666
2667        let mut ctxt = SchematronValidCtxt::new();
2668        let valid = unsafe { schematron_validate_doc(&schema, doc, &mut ctxt) };
2669        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2670
2671        assert!(valid, "Child count check failed: {:?}", ctxt.errors);
2672    }
2673}