Skip to main content

libxml_rs/xml/relaxng/
mod.rs

1//! RELAX NG implementation (§27, §85 Phase 6).
2//!
3//! RELAX NG schema validation. Upstream libxml2 RELAX NG support is known to be
4//! incomplete/non-conformant in places — parity follows the oracle.
5//!
6//! Phase 6: Complete — RELAX NG grammar parsing, document validation,
7//! and C ABI exports are implemented.
8//!
9//! # UPSTREAM-PARITY
10//!
11//! This module implements a functional RELAX NG validator following the
12//! upstream libxml2 observable behavior. The implementation covers:
13//!
14//! - Full XML syntax for RELAX NG grammar definitions
15//! - Pattern-based validation of XML documents
16//! - Name classes for element/attribute matching
17//! - Basic datatype checking for `<data>` and `<value>` patterns
18//! - Reference resolution for `<ref>` and `<define>` patterns
19//! - Include support for external grammars (basic)
20//! - Compact syntax (basic support)
21//!
22//! Deviations from the OASIS RELAX NG specification that match the upstream
23//! libxml2 behavior are intentional.
24//!
25//! # Upstream contract
26//!
27//! Mirrors upstream relaxng.c (SRC-LIBXML2-2.15.0-RELAXNG-C, oracle tree
28//! `oracle/historical/src/libxml2-2.15.0/relaxng.c`): xmlRelaxNG parse/valid
29//! contexts, pattern compilation, name classes and the datatype hooks. Parity
30//! target: the system libxml2 2.15.3 oracle — NOT the OASIS RELAX NG
31//! specification where the two differ.
32//!
33//! # Conceptual behavior
34//!
35//! RELAX NG schema validation: grammar parsing, pattern-based document
36//! validation, name classes for element/attribute matching, datatype checks
37//! for data/value patterns, ref/define resolution, include support and basic
38//! compact syntax. Upstream libxml2 RELAX NG support is known to be
39//! incomplete/non-conformant in places; the module reproduces the oracle
40//! behavior, not the standard.
41//!
42//! # Ownership & safety invariants
43//!
44//! Ownership: schemas own their pattern tree (xmlRelaxNGFree); parser and
45//! valid contexts own their state (xmlRelaxNGFreeParserCtxt /
46//! xmlRelaxNGFreeValidCtxt); the validated document is borrowed. SAFETY:
47//! pattern references are resolved with cycle guards so recursive grammars
48//! cannot loop the validator.
49//!
50//! # Historical quirks & epochs
51//!
52//! RELAX NG landed in the 2.6 validation-era expansion (2003-2004,
53//! atlas/HISTORY.md 1.5); xmlRelaxNGInitTypes is empty after lazy init and
54//! xmlRelaxNGCleanupTypes is a documented no-op (R-000138 no-op set, matching
55//! upstream empty bodies).
56//!
57//! # Deliberate oddities
58//!
59//! Deliberate oddities: deviations from the OASIS specification that match
60//! upstream libxml2 behavior are intentional and kept; the C-API surface
61//! (xmlRelaxNGNewParserCtxt, xmlRelaxNGNewMemParserCtxt, xmlRelaxNGFree,
62//! xmlRelaxNGNewValidCtxt, ...) is exported with upstream signatures.
63//!
64//! # Proving courts
65//!
66//! RELAXNG court family; the data-ABI header-compile court compiles
67//! every relaxng.h declaration against the DSO; dso-loader 25/25; `cargo test
68//! --lib` exercises the validator.
69//!
70//! # Tempting simplifications that would break parity
71//!
72//! The tempting simplification is implementing full OASIS RELAX NG
73//! conformance — it would diverge from upstream libxml2 known non-conformance
74//! and break byte-identical validation output. Do not drop the compact-syntax
75//! handling: upstream accepts it and the oracle surface includes it.
76
77#![allow(
78    missing_docs,
79    non_snake_case,
80    non_camel_case_types,
81    non_upper_case_globals
82)]
83
84use core::ffi::c_void;
85use core::ptr;
86use std::os::raw::{c_char, c_int};
87
88use crate::abi::allocator;
89use crate::abi::structs::*;
90use crate::abi::types::xmlElementType::*;
91
92// ═══════════════════════════════════════════════════════════════════════════════
93// RELAX NG Name Class
94// ═══════════════════════════════════════════════════════════════════════════════
95
96/// RELAX NG name class — determines which element/attribute names a pattern matches.
97///
98/// # UPSTREAM-PARITY
99///
100/// libxml2 defines name classes in `include/relaxng.h` as part of the
101/// pattern structure. This enum provides the same semantics.
102#[derive(Debug, Clone, PartialEq)]
103pub enum RelaxNgNameClass {
104    /// Match a specific name (`<name>`)
105    Name(String),
106    /// Match any name (`<anyName>`)
107    AnyName,
108    /// Match any name in a namespace (`<nsName>`)
109    NsName(String),
110    /// Choice between multiple name classes (`<choice>`)
111    Choice(Vec<RelaxNgNameClass>),
112    /// Exclude a name class (used with anyName/nsName)
113    Except(Box<RelaxNgNameClass>, Box<RelaxNgNameClass>),
114}
115
116impl RelaxNgNameClass {
117    /// Check if a given qualified name matches this name class.
118    pub fn matches(&self, name: &str, ns_uri: Option<&str>) -> bool {
119        match self {
120            RelaxNgNameClass::Name(n) => name == n.as_str(),
121            RelaxNgNameClass::AnyName => true,
122            RelaxNgNameClass::NsName(ns) => {
123                if let Some(uri) = ns_uri {
124                    uri == ns.as_str()
125                } else {
126                    false
127                }
128            }
129            RelaxNgNameClass::Choice(choices) => choices.iter().any(|c| c.matches(name, ns_uri)),
130            RelaxNgNameClass::Except(positive, negative) => {
131                positive.matches(name, ns_uri) && !negative.matches(name, ns_uri)
132            }
133        }
134    }
135}
136
137// ═══════════════════════════════════════════════════════════════════════════════
138// RELAX NG Pattern Types
139// ═══════════════════════════════════════════════════════════════════════════════
140
141/// RELAX NG pattern type — classification for pattern dispatch.
142///
143/// # UPSTREAM-PARITY
144///
145/// Mirrors libxml2's internal pattern type classification.
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub enum RelaxNgPatternType {
148    Element,
149    Attribute,
150    Text,
151    Choice,
152    Sequence,
153    Interleave,
154    ZeroOrMore,
155    OneOrMore,
156    Optional,
157    List,
158    Group,
159    Data,
160    Value,
161    Ref,
162    Define,
163    Grammar,
164    NotAllowed,
165    Empty,
166    ExternalRef,
167    Include,
168    Start,
169}
170
171/// A RELAX NG pattern — the core building block of RELAX NG schemas.
172///
173/// Patterns form a tree structure where composite patterns contain child patterns.
174#[derive(Debug, Clone)]
175pub struct RelaxNgPattern {
176    /// The type of this pattern.
177    pub pattern_type: RelaxNgPatternType,
178    /// Name class (for element/attribute patterns).
179    pub name_class: Option<RelaxNgNameClass>,
180    /// Child patterns.
181    pub children: Vec<RelaxNgPattern>,
182    /// The name for `<ref>` and `<define>` patterns.
183    pub name: Option<String>,
184    /// The namespace URI for nsName or element/attribute.
185    pub ns: Option<String>,
186    /// Datatype for `<data>` patterns.
187    pub datatype: Option<String>,
188    /// Value for `<value>` patterns.
189    pub value: Option<String>,
190    /// Datatype library (e.g., "" for built-in).
191    pub datatype_library: Option<String>,
192}
193
194impl RelaxNgPattern {
195    /// Create a new pattern with the given type.
196    pub const fn new(pattern_type: RelaxNgPatternType) -> Self {
197        Self {
198            pattern_type,
199            name_class: None,
200            children: Vec::new(),
201            name: None,
202            ns: None,
203            datatype: None,
204            value: None,
205            datatype_library: None,
206        }
207    }
208
209    /// Create a simple element pattern with a name.
210    pub fn element(name: &str) -> Self {
211        let mut p = Self::new(RelaxNgPatternType::Element);
212        p.name_class = Some(RelaxNgNameClass::Name(name.to_string()));
213        p
214    }
215
216    /// Create a simple attribute pattern with a name.
217    pub fn attribute(name: &str) -> Self {
218        let mut p = Self::new(RelaxNgPatternType::Attribute);
219        p.name_class = Some(RelaxNgNameClass::Name(name.to_string()));
220        p
221    }
222
223    /// Create a text pattern.
224    pub const fn text() -> Self {
225        Self::new(RelaxNgPatternType::Text)
226    }
227
228    /// Create an empty pattern.
229    pub const fn empty() -> Self {
230        Self::new(RelaxNgPatternType::Empty)
231    }
232
233    /// Create a notAllowed pattern.
234    pub const fn not_allowed() -> Self {
235        Self::new(RelaxNgPatternType::NotAllowed)
236    }
237}
238
239// ═══════════════════════════════════════════════════════════════════════════════
240// RELAX NG Define (named pattern definition)
241// ═══════════════════════════════════════════════════════════════════════════════
242
243/// A named pattern definition (`<define>`).
244#[derive(Debug, Clone)]
245pub struct RelaxNgDefine {
246    /// The name of this definition.
247    pub name: String,
248    /// The pattern body.
249    pub pattern: RelaxNgPattern,
250}
251
252// ═══════════════════════════════════════════════════════════════════════════════
253// RELAX NG Grammar
254// ═══════════════════════════════════════════════════════════════════════════════
255
256/// A RELAX NG grammar — the top-level container for pattern definitions.
257///
258/// Contains named pattern definitions and an optional start pattern.
259#[derive(Debug, Clone)]
260pub struct RelaxNgGrammar {
261    /// Named pattern definitions (`<define>` elements).
262    pub defines: Vec<RelaxNgDefine>,
263    /// The start pattern (`<start>`).
264    pub start: Option<RelaxNgPattern>,
265    /// Included grammars (from `<include>`).
266    pub includes: Vec<RelaxNgGrammar>,
267}
268
269impl RelaxNgGrammar {
270    pub const fn new() -> Self {
271        Self {
272            defines: Vec::new(),
273            start: None,
274            includes: Vec::new(),
275        }
276    }
277
278    /// Look up a named pattern definition.
279    pub fn lookup(&self, name: &str) -> Option<&RelaxNgPattern> {
280        for def in &self.defines {
281            if def.name == name {
282                return Some(&def.pattern);
283            }
284        }
285        // Also search includes
286        for inc in &self.includes {
287            if let Some(p) = inc.lookup(name) {
288                return Some(p);
289            }
290        }
291        None
292    }
293}
294
295impl Default for RelaxNgGrammar {
296    fn default() -> Self {
297        Self::new()
298    }
299}
300
301// ═══════════════════════════════════════════════════════════════════════════════
302// RELAX NG Schema
303// ═══════════════════════════════════════════════════════════════════════════════
304
305/// A compiled RELAX NG schema.
306///
307/// Wraps a grammar and tracks any errors encountered during parsing.
308#[derive(Debug, Clone)]
309pub struct RelaxNgSchema {
310    /// The top-level grammar.
311    pub grammar: RelaxNgGrammar,
312    /// Errors encountered during parsing.
313    pub errors: Vec<String>,
314}
315
316impl RelaxNgSchema {
317    pub const fn new() -> Self {
318        Self {
319            grammar: RelaxNgGrammar::new(),
320            errors: Vec::new(),
321        }
322    }
323}
324
325impl Default for RelaxNgSchema {
326    fn default() -> Self {
327        Self::new()
328    }
329}
330
331// ═══════════════════════════════════════════════════════════════════════════════
332// RELAX NG Validation Context
333// ═══════════════════════════════════════════════════════════════════════════════
334
335/// Validation context for RELAX NG schema validation.
336///
337/// Tracks errors, current element path, and pattern recursion depth
338/// during validation of an XML document against a RELAX NG schema.
339/// Mirrors libxml2's `xmlRelaxNGValidCtxt`.
340#[derive(Debug)]
341pub struct RelaxNgValidCtxt {
342    /// The schema being validated against.
343    pub schema: Option<RelaxNgSchema>,
344    /// Accumulated validation errors.
345    pub errors: Vec<String>,
346    /// Number of validation errors.
347    pub nb_errors: i32,
348    /// Current element path (stack of element names).
349    pub path: Vec<String>,
350    /// Maximum recursion depth for pattern matching.
351    pub depth_max: i32,
352    /// Current recursion depth.
353    pub depth: i32,
354}
355
356impl RelaxNgValidCtxt {
357    pub const fn new() -> Self {
358        Self {
359            schema: None,
360            errors: Vec::new(),
361            nb_errors: 0,
362            path: Vec::new(),
363            depth_max: 256,
364            depth: 0,
365        }
366    }
367
368    /// Record a validation error.
369    pub fn record_error(&mut self, msg: String) {
370        self.errors.push(msg);
371        self.nb_errors += 1;
372    }
373
374    /// Get the current path as a string (e.g., "/root/child").
375    pub fn current_path(&self) -> String {
376        if self.path.is_empty() {
377            "/".to_string()
378        } else {
379            format!("/{}", self.path.join("/"))
380        }
381    }
382}
383
384impl Default for RelaxNgValidCtxt {
385    fn default() -> Self {
386        Self::new()
387    }
388}
389
390// ═══════════════════════════════════════════════════════════════════════════════
391// Internal helpers for RELAX NG parsing
392// ═══════════════════════════════════════════════════════════════════════════════
393
394/// Get the local name of an element node (strip namespace prefix).
395///
396/// # SAFETY
397///
398/// - `node` must be a valid pointer to an _xmlNode or NULL.
399unsafe fn get_local_name(node: *mut _xmlNode) -> String {
400    if node.is_null() {
401        return String::new();
402    }
403    unsafe {
404        let name = (*node).name;
405        if name.is_null() {
406            return String::new();
407        }
408        let mut len = 0;
409        while *name.add(len) != 0 {
410            len += 1;
411        }
412        let slice = std::slice::from_raw_parts(name, len);
413        if let Ok(s) = std::str::from_utf8(slice) {
414            if let Some(pos) = s.find(':') {
415                s[pos + 1..].to_string()
416            } else {
417                s.to_string()
418            }
419        } else {
420            String::new()
421        }
422    }
423}
424
425/// Get the text content of an xmlNode (recursively collects text children).
426///
427/// # SAFETY
428///
429/// - `node` must be a valid pointer to an _xmlNode or NULL.
430unsafe fn get_node_text(node: *mut _xmlNode) -> String {
431    if node.is_null() {
432        return String::new();
433    }
434    let mut result = String::new();
435    unsafe {
436        let mut child = (*node).children;
437        while !child.is_null() {
438            if ((*child).type_ == XML_TEXT_NODE as c_int
439                || (*child).type_ == XML_CDATA_SECTION_NODE as c_int)
440                && !(*child).content.is_null()
441            {
442                let content = (*child).content;
443                let mut len = 0;
444                while *content.add(len) != 0 {
445                    len += 1;
446                }
447                let slice = std::slice::from_raw_parts(content, len);
448                result.push_str(&String::from_utf8_lossy(slice));
449            }
450            child = (*child).next;
451        }
452    }
453    result
454}
455
456/// Get the qualified name of a node (with namespace prefix if available).
457///
458/// # SAFETY
459///
460/// - `node` must be a valid pointer to an _xmlNode or NULL.
461unsafe fn get_node_qname(node: *mut _xmlNode) -> String {
462    if node.is_null() {
463        return String::new();
464    }
465    unsafe {
466        let ns = (*node).ns;
467        let prefix = if !ns.is_null() && !(*ns).prefix.is_null() {
468            let mut len = 0;
469            while *(*ns).prefix.add(len) != 0 {
470                len += 1;
471            }
472            let slice = std::slice::from_raw_parts((*ns).prefix, len);
473            if let Ok(s) = std::str::from_utf8(slice) {
474                format!("{}:", s)
475            } else {
476                String::new()
477            }
478        } else {
479            String::new()
480        };
481
482        let name = (*node).name;
483        if name.is_null() {
484            return String::new();
485        }
486        let mut len = 0;
487        while *name.add(len) != 0 {
488            len += 1;
489        }
490        let slice = std::slice::from_raw_parts(name, len);
491        if let Ok(s) = std::str::from_utf8(slice) {
492            format!("{}{}", prefix, s)
493        } else {
494            String::new()
495        }
496    }
497}
498
499/// Get the namespace URI of a node.
500///
501/// # SAFETY
502///
503/// - `node` must be a valid pointer to an _xmlNode or NULL.
504unsafe fn get_node_ns_uri(node: *mut _xmlNode) -> Option<String> {
505    if node.is_null() {
506        return None;
507    }
508    unsafe {
509        let ns = (*node).ns;
510        if ns.is_null() || (*ns).href.is_null() {
511            return None;
512        }
513        let href = (*ns).href;
514        let mut len = 0;
515        while *href.add(len) != 0 {
516            len += 1;
517        }
518        let slice = std::slice::from_raw_parts(href, len);
519        if let Ok(s) = std::str::from_utf8(slice) {
520            Some(s.to_string())
521        } else {
522            None
523        }
524    }
525}
526
527/// Get an attribute value from an xmlNode.
528///
529/// # SAFETY
530///
531/// - `node` must be a valid pointer to an _xmlNode or NULL.
532unsafe fn get_attr(node: *mut _xmlNode, name: &str) -> Option<String> {
533    if node.is_null() {
534        return None;
535    }
536    unsafe {
537        let mut prop = (*node).properties;
538        while !prop.is_null() {
539            let prop_name = (*prop).name;
540            if !prop_name.is_null() {
541                let mut len = 0;
542                while *prop_name.add(len) != 0 {
543                    len += 1;
544                }
545                let slice = std::slice::from_raw_parts(prop_name, len);
546                if let Ok(s) = std::str::from_utf8(slice) {
547                    if s == name {
548                        return Some(get_node_text(prop as *mut _xmlNode));
549                    }
550                }
551            }
552            prop = (*prop).next;
553        }
554    }
555    None
556}
557
558/// Check if an xmlNode is an element with a given local name.
559///
560/// # SAFETY
561///
562/// - `node` must be a valid pointer to an _xmlNode or NULL.
563#[allow(dead_code)]
564unsafe fn node_is(node: *mut _xmlNode, local_name: &str) -> bool {
565    if node.is_null() {
566        return false;
567    }
568    unsafe {
569        let name = (*node).name;
570        if name.is_null() {
571            return false;
572        }
573        let mut len = 0;
574        while *name.add(len) != 0 {
575            len += 1;
576        }
577        let slice = std::slice::from_raw_parts(name, len);
578        if let Ok(s) = std::str::from_utf8(slice) {
579            let local = if let Some(pos) = s.find(':') {
580                &s[pos + 1..]
581            } else {
582                s
583            };
584            return local == local_name;
585        }
586    }
587    false
588}
589
590// ═══════════════════════════════════════════════════════════════════════════════
591// RELAX NG Schema Parsing
592// ═══════════════════════════════════════════════════════════════════════════════
593
594/// Parse a RELAX NG schema from an XML string.
595///
596/// # UPSTREAM-PARITY
597///
598/// Equivalent to `xmlRelaxNGParse` in libxml2 when given a parser context
599/// created from a memory buffer.
600///
601/// Returns the parsed schema, or an error message on failure.
602pub fn rng_parse(xml_doc: &str) -> Result<RelaxNgSchema, String> {
603    let doc_ptr = unsafe {
604        crate::abi::exports_xml2::xmlReadMemory(
605            xml_doc.as_ptr() as *const c_char,
606            xml_doc.len() as c_int,
607            c"schema.rng".as_ptr() as *const c_char,
608            ptr::null(),
609            0,
610        )
611    };
612
613    if doc_ptr.is_null() {
614        return Err("Failed to parse RELAX NG schema XML document".to_string());
615    }
616
617    let result = unsafe { rng_parse_doc(doc_ptr) };
618    unsafe {
619        crate::abi::exports_xml2::xmlFreeDoc(doc_ptr);
620    }
621    result
622}
623
624/// Parse a RELAX NG schema from a parsed XML document.
625///
626/// # SAFETY
627///
628/// - `doc` must be a valid pointer to an _xmlDoc representing a RELAX NG schema.
629unsafe fn rng_parse_doc(doc: *mut _xmlDoc) -> Result<RelaxNgSchema, String> {
630    unsafe {
631        let root = (*doc).children;
632        if root.is_null() {
633            return Err("RELAX NG document has no root element".to_string());
634        }
635
636        // Find the root element (skip any non-element nodes like comments)
637        let mut root_elem = root;
638        while !root_elem.is_null() && (*root_elem).type_ != XML_ELEMENT_NODE as c_int {
639            root_elem = (*root_elem).next;
640        }
641
642        if root_elem.is_null() {
643            return Err("RELAX NG document has no root element".to_string());
644        }
645
646        let local_name = get_local_name(root_elem);
647        let mut schema = RelaxNgSchema::new();
648
649        match local_name.as_str() {
650            "grammar" => {
651                // Top-level grammar
652                schema.grammar = rng_parse_grammar_node(root_elem, &mut schema);
653                Ok(schema)
654            }
655            "element" | "attribute" | "text" | "choice" | "sequence" | "interleave"
656            | "zeroOrMore" | "oneOrMore" | "optional" | "list" | "group" | "data" | "value"
657            | "ref" | "notAllowed" | "empty" | "externalRef" | "define" | "start" | "include" => {
658                // Single pattern as root (simplified grammar)
659                let pattern = rng_parse_pattern(root_elem, &mut schema);
660                schema.grammar.start = Some(pattern);
661                Ok(schema)
662            }
663            _ => Err(format!("Unknown RELAX NG root element: '{}'", local_name)),
664        }
665    }
666}
667
668/// Parse a `<grammar>` element.
669///
670/// # SAFETY
671///
672/// - `node` must be a valid pointer to a `<grammar>` element node.
673unsafe fn rng_parse_grammar_node(
674    node: *mut _xmlNode,
675    schema: &mut RelaxNgSchema,
676) -> RelaxNgGrammar {
677    unsafe {
678        let mut grammar = RelaxNgGrammar::new();
679
680        let mut child = (*node).children;
681        while !child.is_null() {
682            if (*child).type_ == XML_ELEMENT_NODE as c_int {
683                let local = get_local_name(child);
684                match local.as_str() {
685                    "define" => {
686                        let def = rng_parse_define(child, schema);
687                        grammar.defines.push(def);
688                    }
689                    "start" => {
690                        grammar.start = Some(rng_parse_pattern(child, schema));
691                    }
692                    "include" => {
693                        if let Some(inc) = rng_parse_include(child, schema) {
694                            grammar.includes.push(inc);
695                        }
696                    }
697                    "div" => {
698                        // <div> is a grouping element; recurse into it
699                        let sub_grammar = rng_parse_grammar_node(child, schema);
700                        grammar.defines.extend(sub_grammar.defines);
701                        if sub_grammar.start.is_some() {
702                            grammar.start = sub_grammar.start;
703                        }
704                        grammar.includes.extend(sub_grammar.includes);
705                    }
706                    _ => {
707                        // Unknown element inside grammar — treat as pattern error
708                        schema
709                            .errors
710                            .push(format!("Unexpected element '<{}>' in grammar", local));
711                    }
712                }
713            }
714            child = (*child).next;
715        }
716
717        grammar
718    }
719}
720
721/// Parse a `<define>` element.
722///
723/// # SAFETY
724///
725/// - `node` must be a valid pointer to a `<define>` element node.
726unsafe fn rng_parse_define(node: *mut _xmlNode, schema: &mut RelaxNgSchema) -> RelaxNgDefine {
727    unsafe {
728        let name = get_attr(node, "name").unwrap_or_default();
729        let pattern = rng_parse_pattern(node, schema);
730        RelaxNgDefine { name, pattern }
731    }
732}
733
734/// Parse an `<include>` element.
735///
736/// # SAFETY
737///
738/// - `node` must be a valid pointer to an `<include>` element node.
739unsafe fn rng_parse_include(
740    node: *mut _xmlNode,
741    _schema: &mut RelaxNgSchema,
742) -> Option<RelaxNgGrammar> {
743    unsafe {
744        let href = get_attr(node, "href");
745        if let Some(url) = href {
746            // Try to load and parse the external grammar
747            // For basic support, we attempt to read the file
748            let url_c = std::ffi::CString::new(url.clone()).ok()?;
749            let doc = crate::abi::exports_xml2::xmlParseFile(url_c.as_ptr());
750            if doc.is_null() {
751                return None;
752            }
753            let mut inc_schema = RelaxNgSchema::new();
754            let grammar = rng_parse_grammar_node(
755                {
756                    let mut root = (*doc).children;
757                    while !root.is_null() && (*root).type_ != XML_ELEMENT_NODE as c_int {
758                        root = (*root).next;
759                    }
760                    root
761                },
762                &mut inc_schema,
763            );
764            crate::abi::exports_xml2::xmlFreeDoc(doc);
765            Some(grammar)
766        } else {
767            // Inline grammar in include
768            let mut inc_schema = RelaxNgSchema::new();
769            let grammar = rng_parse_grammar_node(node, &mut inc_schema);
770            Some(grammar)
771        }
772    }
773}
774
775/// Parse a pattern from an element node. Dispatches based on element name.
776///
777/// # SAFETY
778///
779/// - `node` must be a valid pointer to an XML element node.
780unsafe fn rng_parse_pattern(node: *mut _xmlNode, schema: &mut RelaxNgSchema) -> RelaxNgPattern {
781    unsafe {
782        let local = get_local_name(node);
783
784        match local.as_str() {
785            "element" => rng_parse_element_pattern(node, schema),
786            "attribute" => rng_parse_attribute_pattern(node, schema),
787            "text" => RelaxNgPattern::text(),
788            "empty" => RelaxNgPattern::empty(),
789            "notAllowed" => RelaxNgPattern::not_allowed(),
790            "choice" => rng_parse_composite_pattern(node, RelaxNgPatternType::Choice, schema),
791            "sequence" => rng_parse_composite_pattern(node, RelaxNgPatternType::Sequence, schema),
792            "interleave" => {
793                rng_parse_composite_pattern(node, RelaxNgPatternType::Interleave, schema)
794            }
795            "zeroOrMore" => rng_parse_unary_pattern(node, RelaxNgPatternType::ZeroOrMore, schema),
796            "oneOrMore" => rng_parse_unary_pattern(node, RelaxNgPatternType::OneOrMore, schema),
797            "optional" => rng_parse_unary_pattern(node, RelaxNgPatternType::Optional, schema),
798            "list" => rng_parse_unary_pattern(node, RelaxNgPatternType::List, schema),
799            "group" => rng_parse_composite_pattern(node, RelaxNgPatternType::Group, schema),
800            "data" => rng_parse_data_pattern(node, schema),
801            "value" => rng_parse_value_pattern(node, schema),
802            "ref" => rng_parse_ref_pattern(node),
803            "externalRef" => rng_parse_external_ref(node, schema),
804            "define" | "start" | "grammar" | "include" | "div" => {
805                // These are grammar-level elements; return the content pattern
806                let mut child = (*node).children;
807                let mut result = RelaxNgPattern::empty();
808                while !child.is_null() {
809                    if (*child).type_ == XML_ELEMENT_NODE as c_int {
810                        result = rng_parse_pattern(child, schema);
811                        break;
812                    }
813                    child = (*child).next;
814                }
815                result
816            }
817            _ => {
818                // Unknown element — treat as empty pattern
819                schema
820                    .errors
821                    .push(format!("Unknown pattern element '<{}>'", local));
822                RelaxNgPattern::empty()
823            }
824        }
825    }
826}
827
828/// Parse an `<element>` pattern.
829///
830/// # SAFETY
831///
832/// - `node` must be a valid pointer to an `<element>` element node.
833unsafe fn rng_parse_element_pattern(
834    node: *mut _xmlNode,
835    schema: &mut RelaxNgSchema,
836) -> RelaxNgPattern {
837    unsafe {
838        let mut pattern = RelaxNgPattern::new(RelaxNgPatternType::Element);
839
840        // Parse name class from name attribute or child <name>, <anyName>, <nsName>, <choice>
841        let name_attr = get_attr(node, "name");
842        pattern.name = name_attr.clone();
843
844        if let Some(ref n) = name_attr {
845            pattern.name_class = Some(RelaxNgNameClass::Name(n.clone()));
846        }
847
848        // Parse children for name class and content pattern
849        let mut child = (*node).children;
850        let mut content_found = false;
851
852        while !child.is_null() {
853            if (*child).type_ == XML_ELEMENT_NODE as c_int {
854                let child_local = get_local_name(child);
855
856                match child_local.as_str() {
857                    "name" => {
858                        let text = get_node_text(child);
859                        if !text.is_empty() {
860                            pattern.name_class =
861                                Some(RelaxNgNameClass::Name(text.trim().to_string()));
862                        }
863                    }
864                    "anyName" => {
865                        pattern.name_class = Some(rng_parse_any_name(child));
866                    }
867                    "nsName" => {
868                        pattern.name_class = Some(rng_parse_ns_name(child));
869                    }
870                    "choice" if pattern.name_class.is_none() => {
871                        // Name class choice (only before content)
872                        pattern.name_class = Some(rng_parse_name_class_choice(child));
873                    }
874                    _ => {
875                        // Content pattern
876                        if !content_found {
877                            pattern.children.push(rng_parse_pattern(child, schema));
878                            content_found = true;
879                        }
880                    }
881                }
882            }
883            child = (*child).next;
884        }
885
886        pattern
887    }
888}
889
890/// Parse an `<attribute>` pattern.
891///
892/// # SAFETY
893///
894/// - `node` must be a valid pointer to an `<attribute>` element node.
895unsafe fn rng_parse_attribute_pattern(
896    node: *mut _xmlNode,
897    schema: &mut RelaxNgSchema,
898) -> RelaxNgPattern {
899    unsafe {
900        let mut pattern = RelaxNgPattern::new(RelaxNgPatternType::Attribute);
901
902        // Parse name class
903        let name_attr = get_attr(node, "name");
904        pattern.name = name_attr.clone();
905
906        if let Some(ref n) = name_attr {
907            pattern.name_class = Some(RelaxNgNameClass::Name(n.clone()));
908        }
909
910        // Parse children
911        let mut child = (*node).children;
912        while !child.is_null() {
913            if (*child).type_ == XML_ELEMENT_NODE as c_int {
914                let child_local = get_local_name(child);
915
916                match child_local.as_str() {
917                    "name" => {
918                        let text = get_node_text(child);
919                        if !text.is_empty() {
920                            pattern.name_class =
921                                Some(RelaxNgNameClass::Name(text.trim().to_string()));
922                        }
923                    }
924                    "anyName" => {
925                        pattern.name_class = Some(rng_parse_any_name(child));
926                    }
927                    "nsName" => {
928                        pattern.name_class = Some(rng_parse_ns_name(child));
929                    }
930                    "choice" if pattern.name_class.is_none() => {
931                        pattern.name_class = Some(rng_parse_name_class_choice(child));
932                    }
933                    _ => {
934                        // Content pattern (text, data, etc.)
935                        pattern.children.push(rng_parse_pattern(child, schema));
936                    }
937                }
938            }
939            child = (*child).next;
940        }
941
942        pattern
943    }
944}
945
946/// Parse an `<anyName>` element (possibly with `<except>`).
947///
948/// # SAFETY
949///
950/// - `node` must be a valid pointer to an `<anyName>` element node.
951unsafe fn rng_parse_any_name(node: *mut _xmlNode) -> RelaxNgNameClass {
952    unsafe {
953        // Check for <except> child
954        let mut child = (*node).children;
955        while !child.is_null() {
956            if (*child).type_ == XML_ELEMENT_NODE as c_int && get_local_name(child) == "except" {
957                let except_nc = rng_parse_name_class_content(child);
958                return RelaxNgNameClass::Except(
959                    Box::new(RelaxNgNameClass::AnyName),
960                    Box::new(except_nc),
961                );
962            }
963            child = (*child).next;
964        }
965        RelaxNgNameClass::AnyName
966    }
967}
968
969/// Parse an `<nsName>` element (possibly with `<except>`).
970///
971/// # SAFETY
972///
973/// - `node` must be a valid pointer to an `<nsName>` element node.
974unsafe fn rng_parse_ns_name(node: *mut _xmlNode) -> RelaxNgNameClass {
975    unsafe {
976        let ns = get_attr(node, "ns").unwrap_or_default();
977
978        // Check for <except> child
979        let mut child = (*node).children;
980        while !child.is_null() {
981            if (*child).type_ == XML_ELEMENT_NODE as c_int && get_local_name(child) == "except" {
982                let except_nc = rng_parse_name_class_content(child);
983                return RelaxNgNameClass::Except(
984                    Box::new(RelaxNgNameClass::NsName(ns)),
985                    Box::new(except_nc),
986                );
987            }
988            child = (*child).next;
989        }
990
991        RelaxNgNameClass::NsName(ns)
992    }
993}
994
995/// Parse name class children of a `<choice>` element used as name class.
996///
997/// # SAFETY
998///
999/// - `node` must be a valid pointer to an element node containing name class children.
1000unsafe fn rng_parse_name_class_choice(node: *mut _xmlNode) -> RelaxNgNameClass {
1001    unsafe {
1002        let mut choices = Vec::new();
1003        let mut child = (*node).children;
1004        while !child.is_null() {
1005            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1006                choices.push(rng_parse_name_class_item(child));
1007            }
1008            child = (*child).next;
1009        }
1010        if choices.len() == 1 {
1011            choices.remove(0)
1012        } else {
1013            RelaxNgNameClass::Choice(choices)
1014        }
1015    }
1016}
1017
1018/// Parse a single name class item from a name class context.
1019///
1020/// # SAFETY
1021///
1022/// - `node` must be a valid pointer to an element node.
1023unsafe fn rng_parse_name_class_item(node: *mut _xmlNode) -> RelaxNgNameClass {
1024    unsafe {
1025        let local = get_local_name(node);
1026        match local.as_str() {
1027            "name" => {
1028                let text = get_node_text(node);
1029                RelaxNgNameClass::Name(text.trim().to_string())
1030            }
1031            "anyName" => rng_parse_any_name(node),
1032            "nsName" => rng_parse_ns_name(node),
1033            "choice" => rng_parse_name_class_choice(node),
1034            _ => {
1035                // Default: treat as name
1036                let text = get_node_text(node);
1037                if text.trim().is_empty() {
1038                    RelaxNgNameClass::AnyName
1039                } else {
1040                    RelaxNgNameClass::Name(text.trim().to_string())
1041                }
1042            }
1043        }
1044    }
1045}
1046
1047/// Parse name class content from an `<except>` or similar element.
1048///
1049/// # SAFETY
1050///
1051/// - `node` must be a valid pointer to an element node.
1052unsafe fn rng_parse_name_class_content(node: *mut _xmlNode) -> RelaxNgNameClass {
1053    unsafe {
1054        let mut names = Vec::new();
1055        let mut child = (*node).children;
1056        while !child.is_null() {
1057            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1058                names.push(rng_parse_name_class_item(child));
1059            }
1060            child = (*child).next;
1061        }
1062        if names.is_empty() {
1063            RelaxNgNameClass::AnyName
1064        } else if names.len() == 1 {
1065            names.remove(0)
1066        } else {
1067            RelaxNgNameClass::Choice(names)
1068        }
1069    }
1070}
1071
1072/// Parse a composite pattern (sequence, choice, interleave, group).
1073///
1074/// # SAFETY
1075///
1076/// - `node` must be a valid pointer to an element node.
1077unsafe fn rng_parse_composite_pattern(
1078    node: *mut _xmlNode,
1079    pattern_type: RelaxNgPatternType,
1080    schema: &mut RelaxNgSchema,
1081) -> RelaxNgPattern {
1082    unsafe {
1083        let mut pattern = RelaxNgPattern::new(pattern_type);
1084
1085        let mut child = (*node).children;
1086        while !child.is_null() {
1087            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1088                pattern.children.push(rng_parse_pattern(child, schema));
1089            }
1090            child = (*child).next;
1091        }
1092
1093        pattern
1094    }
1095}
1096
1097/// Parse a unary pattern (zeroOrMore, oneOrMore, optional, list).
1098///
1099/// # SAFETY
1100///
1101/// - `node` must be a valid pointer to an element node.
1102unsafe fn rng_parse_unary_pattern(
1103    node: *mut _xmlNode,
1104    pattern_type: RelaxNgPatternType,
1105    schema: &mut RelaxNgSchema,
1106) -> RelaxNgPattern {
1107    unsafe {
1108        let mut pattern = RelaxNgPattern::new(pattern_type);
1109
1110        let mut child = (*node).children;
1111        while !child.is_null() {
1112            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1113                pattern.children.push(rng_parse_pattern(child, schema));
1114                // Only take the first child pattern for unary patterns
1115                break;
1116            }
1117            child = (*child).next;
1118        }
1119
1120        pattern
1121    }
1122}
1123
1124/// Parse a `<data>` pattern.
1125///
1126/// # SAFETY
1127///
1128/// - `node` must be a valid pointer to a `<data>` element node.
1129unsafe fn rng_parse_data_pattern(
1130    node: *mut _xmlNode,
1131    _schema: &mut RelaxNgSchema,
1132) -> RelaxNgPattern {
1133    unsafe {
1134        let mut pattern = RelaxNgPattern::new(RelaxNgPatternType::Data);
1135        pattern.datatype = get_attr(node, "type");
1136        pattern.datatype_library = get_attr(node, "datatypeLibrary");
1137
1138        // For basic support, we store the type and don't process params in detail
1139        pattern
1140    }
1141}
1142
1143/// Parse a `<value>` pattern.
1144///
1145/// # SAFETY
1146///
1147/// - `node` must be a valid pointer to a `<value>` element node.
1148unsafe fn rng_parse_value_pattern(
1149    node: *mut _xmlNode,
1150    _schema: &mut RelaxNgSchema,
1151) -> RelaxNgPattern {
1152    unsafe {
1153        let mut pattern = RelaxNgPattern::new(RelaxNgPatternType::Value);
1154        pattern.datatype = get_attr(node, "type");
1155        pattern.datatype_library = get_attr(node, "datatypeLibrary");
1156        pattern.value = Some(get_node_text(node).trim().to_string());
1157
1158        pattern
1159    }
1160}
1161
1162/// Parse a `<ref>` pattern.
1163///
1164/// # SAFETY
1165///
1166/// - `node` must be a valid pointer to a `<ref>` element node.
1167unsafe fn rng_parse_ref_pattern(node: *mut _xmlNode) -> RelaxNgPattern {
1168    unsafe {
1169        let mut pattern = RelaxNgPattern::new(RelaxNgPatternType::Ref);
1170        pattern.name = get_attr(node, "name");
1171        pattern
1172    }
1173}
1174
1175/// Parse an `<externalRef>` pattern.
1176///
1177/// # SAFETY
1178///
1179/// - `node` must be a valid pointer to an `<externalRef>` element node.
1180unsafe fn rng_parse_external_ref(
1181    node: *mut _xmlNode,
1182    _schema: &mut RelaxNgSchema,
1183) -> RelaxNgPattern {
1184    unsafe {
1185        let mut pattern = RelaxNgPattern::new(RelaxNgPatternType::ExternalRef);
1186        let href = get_attr(node, "href");
1187        pattern.name = href;
1188        pattern
1189    }
1190}
1191
1192// ═══════════════════════════════════════════════════════════════════════════════
1193// RELAX NG Validation Logic
1194// ═══════════════════════════════════════════════════════════════════════════════
1195
1196/// Validate an XML document against a RELAX NG schema.
1197///
1198/// Returns `true` if the document is valid.
1199///
1200/// # SAFETY
1201///
1202/// - `schema` must be a valid reference to a parsed schema.
1203/// - `doc` must be a valid pointer to an _xmlDoc or NULL.
1204/// - `ctxt` must be a valid mutable reference to a validation context.
1205pub unsafe fn rng_validate_doc(
1206    schema: &RelaxNgSchema,
1207    doc: *mut _xmlDoc,
1208    ctxt: &mut RelaxNgValidCtxt,
1209) -> bool {
1210    unsafe {
1211        if doc.is_null() {
1212            ctxt.record_error("Document is null".to_string());
1213            return false;
1214        }
1215
1216        let root = (*doc).children;
1217        if root.is_null() {
1218            ctxt.record_error("Document has no children".to_string());
1219            return false;
1220        }
1221
1222        // Find the root element
1223        let mut root_elem = root;
1224        while !root_elem.is_null() && (*root_elem).type_ != XML_ELEMENT_NODE as c_int {
1225            root_elem = (*root_elem).next;
1226        }
1227
1228        if root_elem.is_null() {
1229            ctxt.record_error("Document has no root element".to_string());
1230            return false;
1231        }
1232
1233        // Get the start pattern
1234        let start_pattern = match &schema.grammar.start {
1235            Some(p) => p,
1236            None => {
1237                ctxt.record_error("Schema has no start pattern".to_string());
1238                return false;
1239            }
1240        };
1241
1242        ctxt.path.clear();
1243        let valid = rng_validate_pattern(start_pattern, root_elem, schema, ctxt);
1244
1245        // Check for remaining unmatched errors
1246        valid
1247    }
1248}
1249
1250/// Validate a pattern against a node.
1251///
1252/// # SAFETY
1253///
1254/// - `node` must be a valid pointer to an _xmlNode or NULL.
1255fn rng_validate_pattern(
1256    pattern: &RelaxNgPattern,
1257    node: *mut _xmlNode,
1258    schema: &RelaxNgSchema,
1259    ctxt: &mut RelaxNgValidCtxt,
1260) -> bool {
1261    unsafe {
1262        if ctxt.depth >= ctxt.depth_max {
1263            ctxt.record_error("Maximum validation depth exceeded".to_string());
1264            return false;
1265        }
1266        ctxt.depth += 1;
1267
1268        let result = match pattern.pattern_type {
1269            RelaxNgPatternType::Element => {
1270                rng_validate_element_pattern(pattern, node, schema, ctxt)
1271            }
1272            RelaxNgPatternType::Attribute => {
1273                rng_validate_attribute_pattern(pattern, node, schema, ctxt)
1274            }
1275            RelaxNgPatternType::Text => rng_validate_text_pattern(node, ctxt),
1276            RelaxNgPatternType::Empty => rng_validate_empty_pattern(node, ctxt),
1277            RelaxNgPatternType::NotAllowed => {
1278                let name = get_node_qname(node);
1279                ctxt.record_error(format!(
1280                    "Element '{}' is not allowed at '{}'",
1281                    name,
1282                    ctxt.current_path()
1283                ));
1284                false
1285            }
1286            RelaxNgPatternType::Choice => rng_validate_choice_pattern(pattern, node, schema, ctxt),
1287            RelaxNgPatternType::Sequence => {
1288                rng_validate_sequence_pattern(pattern, node, schema, ctxt)
1289            }
1290            RelaxNgPatternType::Interleave => {
1291                rng_validate_interleave_pattern(pattern, node, schema, ctxt)
1292            }
1293            RelaxNgPatternType::ZeroOrMore => {
1294                rng_validate_zero_or_more(pattern, node, schema, ctxt)
1295            }
1296            RelaxNgPatternType::OneOrMore => rng_validate_one_or_more(pattern, node, schema, ctxt),
1297            RelaxNgPatternType::Optional => {
1298                rng_validate_optional_pattern(pattern, node, schema, ctxt)
1299            }
1300            RelaxNgPatternType::List => rng_validate_list_pattern(pattern, node, schema, ctxt),
1301            RelaxNgPatternType::Group => rng_validate_group_pattern(pattern, node, schema, ctxt),
1302            RelaxNgPatternType::Data => rng_validate_data_pattern(pattern, node, ctxt),
1303            RelaxNgPatternType::Value => rng_validate_value_pattern(pattern, node, ctxt),
1304            RelaxNgPatternType::Ref => rng_validate_ref_pattern(pattern, node, schema, ctxt),
1305            RelaxNgPatternType::ExternalRef => {
1306                // External refs are resolved during parsing; treat as empty
1307                rng_validate_empty_pattern(node, ctxt)
1308            }
1309            RelaxNgPatternType::Define
1310            | RelaxNgPatternType::Grammar
1311            | RelaxNgPatternType::Start
1312            | RelaxNgPatternType::Include => {
1313                // These shouldn't appear during validation; treat as pass-through
1314                rng_validate_children(pattern, node, schema, ctxt)
1315            }
1316        };
1317
1318        ctxt.depth -= 1;
1319        result
1320    }
1321}
1322
1323/// Validate a pattern's children against a node's children.
1324///
1325/// # SAFETY
1326///
1327/// - `node` must be a valid pointer to an _xmlNode or NULL.
1328fn rng_validate_children(
1329    pattern: &RelaxNgPattern,
1330    node: *mut _xmlNode,
1331    schema: &RelaxNgSchema,
1332    ctxt: &mut RelaxNgValidCtxt,
1333) -> bool {
1334    {
1335        if pattern.children.is_empty() {
1336            return true;
1337        }
1338        // Validate each child pattern against the same node
1339        let mut valid = true;
1340        for child in &pattern.children {
1341            valid &= rng_validate_pattern(child, node, schema, ctxt);
1342        }
1343        valid
1344    }
1345}
1346
1347/// Validate an element pattern against an element node.
1348///
1349/// # SAFETY
1350///
1351/// - `node` must be a valid pointer to an _xmlNode or NULL.
1352fn rng_validate_element_pattern(
1353    pattern: &RelaxNgPattern,
1354    node: *mut _xmlNode,
1355    schema: &RelaxNgSchema,
1356    ctxt: &mut RelaxNgValidCtxt,
1357) -> bool {
1358    unsafe {
1359        if node.is_null() || (*node).type_ != XML_ELEMENT_NODE as c_int {
1360            return false;
1361        }
1362
1363        let node_name = get_node_qname(node);
1364        let ns_uri = get_node_ns_uri(node);
1365
1366        // Check if the node name matches the element pattern's name class
1367        if let Some(ref nc) = pattern.name_class {
1368            if !nc.matches(&node_name, ns_uri.as_deref()) {
1369                let pat_name = pattern.name.as_deref().unwrap_or("?");
1370                ctxt.record_error(format!(
1371                    "Element '{}' does not match expected element pattern '{}' at '{}'",
1372                    node_name,
1373                    pat_name,
1374                    ctxt.current_path()
1375                ));
1376                return false;
1377            }
1378        }
1379
1380        // Push the element name onto the path
1381        ctxt.path.push(node_name.clone());
1382
1383        // Validate child patterns against this element
1384        let mut valid = true;
1385        if pattern.children.is_empty() {
1386            // No content pattern — element must be empty
1387            let mut child = (*node).children;
1388            while !child.is_null() {
1389                if (*child).type_ == XML_ELEMENT_NODE as c_int {
1390                    let child_name = get_node_qname(child);
1391                    ctxt.record_error(format!(
1392                        "Unexpected child element '{}' in empty element '{}' at '{}'",
1393                        child_name,
1394                        node_name,
1395                        ctxt.current_path()
1396                    ));
1397                    valid = false;
1398                }
1399                child = (*child).next;
1400            }
1401        } else {
1402            // Validate the content pattern against this element
1403            // For element patterns, the child pattern validates the element's content
1404            for child_pat in &pattern.children {
1405                valid &= rng_validate_pattern(child_pat, node, schema, ctxt);
1406            }
1407        }
1408
1409        ctxt.path.pop();
1410        valid
1411    }
1412}
1413
1414/// Validate an attribute pattern.
1415///
1416/// # SAFETY
1417///
1418/// - `node` must be a valid pointer to an _xmlNode or NULL.
1419fn rng_validate_attribute_pattern(
1420    pattern: &RelaxNgPattern,
1421    node: *mut _xmlNode,
1422    _schema: &RelaxNgSchema,
1423    ctxt: &mut RelaxNgValidCtxt,
1424) -> bool {
1425    unsafe {
1426        if node.is_null() || (*node).type_ != XML_ELEMENT_NODE as c_int {
1427            return false;
1428        }
1429
1430        // Get the attribute name from the pattern's name class
1431        let attr_name = match &pattern.name_class {
1432            Some(RelaxNgNameClass::Name(n)) => n.clone(),
1433            Some(RelaxNgNameClass::AnyName) => {
1434                // Any attribute is allowed — just check that there's at least one
1435                // attribute that matches (or return true if the attribute is optional)
1436                // For now, we just return true for anyName since we can't know which
1437                // attribute to check
1438                return true;
1439            }
1440            Some(RelaxNgNameClass::NsName(ns)) => {
1441                // Any attribute in the given namespace.
1442                // Check if there's an attribute with a matching namespace.
1443                let mut prop = (*node).properties;
1444                while !prop.is_null() {
1445                    let prop_ns = get_node_ns_uri(prop as *mut _xmlNode);
1446                    if let Some(ref uri) = prop_ns {
1447                        if uri == ns {
1448                            // Validate content pattern against attribute value
1449                            if let Some(content) = &pattern.children.first() {
1450                                let val = get_node_text(prop as *mut _xmlNode);
1451                                let valid = match content.pattern_type {
1452                                    RelaxNgPatternType::Text => true,
1453                                    RelaxNgPatternType::Data => rng_validate_datatype_value(
1454                                        content.datatype.as_deref(),
1455                                        &val,
1456                                    ),
1457                                    RelaxNgPatternType::Value => {
1458                                        content.value.as_deref() == Some(&val)
1459                                    }
1460                                    _ => true,
1461                                };
1462                                if !valid {
1463                                    ctxt.record_error(format!(
1464                                        "Attribute '{}' has invalid value at '{}'",
1465                                        prop_ns.unwrap_or_default(),
1466                                        ctxt.current_path()
1467                                    ));
1468                                    return false;
1469                                }
1470                            }
1471                            return true;
1472                        }
1473                    }
1474                    prop = (*prop).next;
1475                }
1476                // No matching attribute found — attribute is required
1477                // (In RELAX NG, attributes are implicitly required)
1478                ctxt.record_error(format!(
1479                    "Required attribute in namespace '{}' is missing at '{}'",
1480                    ns,
1481                    ctxt.current_path()
1482                ));
1483                return false;
1484            }
1485            _ => {
1486                // Complex name class — just check if any attribute matches
1487                // This is a simplified check
1488                return true;
1489            }
1490        };
1491
1492        // Check if the attribute exists on the element
1493        let attr_value = get_attr(node, &attr_name);
1494
1495        match attr_value {
1496            Some(val) => {
1497                // Validate content pattern against attribute value
1498                if let Some(content) = pattern.children.first() {
1499                    let valid = match content.pattern_type {
1500                        RelaxNgPatternType::Text => true,
1501                        RelaxNgPatternType::Data => {
1502                            rng_validate_datatype_value(content.datatype.as_deref(), &val)
1503                        }
1504                        RelaxNgPatternType::Value => content.value.as_deref() == Some(&val),
1505                        _ => true,
1506                    };
1507                    if !valid {
1508                        ctxt.record_error(format!(
1509                            "Attribute '{}' has invalid value '{}' at '{}'",
1510                            attr_name,
1511                            val,
1512                            ctxt.current_path()
1513                        ));
1514                        return false;
1515                    }
1516                }
1517                true
1518            }
1519            None => {
1520                // Attribute not found — only error if the pattern requires it
1521                // (In RELAX NG, attributes are implicitly required unless wrapped in optional)
1522                ctxt.record_error(format!(
1523                    "Required attribute '{}' is missing at '{}'",
1524                    attr_name,
1525                    ctxt.current_path()
1526                ));
1527                false
1528            }
1529        }
1530    }
1531}
1532
1533/// Validate a text pattern against text content.
1534///
1535/// # SAFETY
1536///
1537/// - `node` must be a valid pointer to an _xmlNode or NULL.
1538const fn rng_validate_text_pattern(node: *mut _xmlNode, _ctxt: &mut RelaxNgValidCtxt) -> bool {
1539    {
1540        if node.is_null() {
1541            return false;
1542        }
1543        // Text pattern matches any text content (or mixed content with elements)
1544        // In RELAX NG, text allows any text content
1545        true
1546    }
1547}
1548
1549/// Validate an empty pattern.
1550///
1551/// # SAFETY
1552///
1553/// - `node` must be a valid pointer to an _xmlNode or NULL.
1554fn rng_validate_empty_pattern(node: *mut _xmlNode, ctxt: &mut RelaxNgValidCtxt) -> bool {
1555    unsafe {
1556        if node.is_null() {
1557            return true;
1558        }
1559        // Empty pattern — the element must have no element children
1560        let mut child = (*node).children;
1561        while !child.is_null() {
1562            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1563                let child_name = get_node_qname(child);
1564                ctxt.record_error(format!(
1565                    "Unexpected child element '{}' in empty content at '{}'",
1566                    child_name,
1567                    ctxt.current_path()
1568                ));
1569                return false;
1570            }
1571            child = (*child).next;
1572        }
1573        true
1574    }
1575}
1576
1577/// Validate a choice pattern.
1578///
1579/// # SAFETY
1580///
1581/// - `node` must be a valid pointer to an _xmlNode or NULL.
1582fn rng_validate_choice_pattern(
1583    pattern: &RelaxNgPattern,
1584    node: *mut _xmlNode,
1585    schema: &RelaxNgSchema,
1586    ctxt: &mut RelaxNgValidCtxt,
1587) -> bool {
1588    unsafe {
1589        if pattern.children.is_empty() {
1590            return false;
1591        }
1592
1593        // At least one choice must match
1594        let mut last_error = String::new();
1595        for child in &pattern.children {
1596            let saved_errors = ctxt.errors.len();
1597            let saved_nb = ctxt.nb_errors;
1598
1599            if rng_validate_pattern(child, node, schema, ctxt) {
1600                // Restore errors from failed choices
1601                // (errors from failed branches should be discarded)
1602                return true;
1603            }
1604
1605            // Capture the last error for reporting
1606            if ctxt.errors.len() > saved_errors {
1607                last_error = ctxt.errors.last().unwrap().clone();
1608            }
1609
1610            // Restore error state (choice means at least one alternative must pass)
1611            ctxt.errors.truncate(saved_errors);
1612            ctxt.nb_errors = saved_nb;
1613        }
1614
1615        // If we got here, none of the choices matched
1616        let node_name = if node.is_null() {
1617            "null".to_string()
1618        } else {
1619            get_node_qname(node)
1620        };
1621        ctxt.record_error(format!(
1622            "No choice pattern matched for '{}' at '{}'. Last error: {}",
1623            node_name,
1624            ctxt.current_path(),
1625            last_error
1626        ));
1627        false
1628    }
1629}
1630
1631/// Validate a sequence pattern.
1632///
1633/// # SAFETY
1634///
1635/// - `node` must be a valid pointer to an _xmlNode or NULL.
1636fn rng_validate_sequence_pattern(
1637    pattern: &RelaxNgPattern,
1638    node: *mut _xmlNode,
1639    schema: &RelaxNgSchema,
1640    ctxt: &mut RelaxNgValidCtxt,
1641) -> bool {
1642    unsafe {
1643        if node.is_null() {
1644            return pattern.children.is_empty();
1645        }
1646
1647        let mut valid = true;
1648
1649        // For sequence validation, we validate each child pattern against
1650        // the node's children in order.
1651        // Collect element children of the node
1652        let mut child_nodes: Vec<*mut _xmlNode> = Vec::new();
1653        let mut child = (*node).children;
1654        while !child.is_null() {
1655            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1656                child_nodes.push(child);
1657            }
1658            child = (*child).next;
1659        }
1660
1661        // Simple sequence matching: validate each child pattern
1662        // against corresponding child nodes
1663        let mut child_idx = 0;
1664        for child_pat in &pattern.children {
1665            if child_idx >= child_nodes.len() {
1666                // Check if the pattern is optional or zero-or-more
1667                match child_pat.pattern_type {
1668                    RelaxNgPatternType::Optional | RelaxNgPatternType::ZeroOrMore => {
1669                        // These are fine to have no matching children
1670                        continue;
1671                    }
1672                    RelaxNgPatternType::OneOrMore => {
1673                        ctxt.record_error(format!(
1674                            "Expected at least one matching child for oneOrMore at '{}'",
1675                            ctxt.current_path()
1676                        ));
1677                        valid = false;
1678                        continue;
1679                    }
1680                    _ => {
1681                        ctxt.record_error(format!(
1682                            "Expected more child elements for sequence at '{}'",
1683                            ctxt.current_path()
1684                        ));
1685                        valid = false;
1686                        continue;
1687                    }
1688                }
1689            }
1690
1691            let child_node = child_nodes[child_idx];
1692            valid &= rng_validate_pattern(child_pat, child_node, schema, ctxt);
1693            child_idx += 1;
1694        }
1695
1696        // Check for extra children
1697        if child_idx < child_nodes.len() {
1698            let extra_name = get_node_qname(child_nodes[child_idx]);
1699            ctxt.record_error(format!(
1700                "Unexpected extra element '{}' in sequence at '{}'",
1701                extra_name,
1702                ctxt.current_path()
1703            ));
1704            valid = false;
1705        }
1706
1707        valid
1708    }
1709}
1710
1711/// Validate an interleave pattern.
1712///
1713/// # SAFETY
1714///
1715/// - `node` must be a valid pointer to an _xmlNode or NULL.
1716fn rng_validate_interleave_pattern(
1717    pattern: &RelaxNgPattern,
1718    node: *mut _xmlNode,
1719    schema: &RelaxNgSchema,
1720    ctxt: &mut RelaxNgValidCtxt,
1721) -> bool {
1722    unsafe {
1723        if node.is_null() {
1724            return pattern.children.is_empty();
1725        }
1726
1727        // Interleave means child patterns can match in any order.
1728        // For simplicity, we validate each child pattern against a fresh
1729        // traversal of the node's children.
1730        let mut valid = true;
1731
1732        for child_pat in &pattern.children {
1733            // For each pattern in the interleave, check if there's at least
1734            // one child node that matches
1735            let mut child = (*node).children;
1736            let mut matched = false;
1737
1738            while !child.is_null() {
1739                if (*child).type_ == XML_ELEMENT_NODE as c_int {
1740                    let saved_errors = ctxt.errors.len();
1741                    let saved_nb = ctxt.nb_errors;
1742
1743                    if rng_validate_pattern(child_pat, child, schema, ctxt) {
1744                        matched = true;
1745                        break;
1746                    }
1747
1748                    // Restore errors for this attempt
1749                    ctxt.errors.truncate(saved_errors);
1750                    ctxt.nb_errors = saved_nb;
1751                }
1752                child = (*child).next;
1753            }
1754
1755            if !matched {
1756                // Check if pattern is optional
1757                match child_pat.pattern_type {
1758                    RelaxNgPatternType::Optional | RelaxNgPatternType::ZeroOrMore => {
1759                        // Optional patterns can be absent
1760                    }
1761                    _ => {
1762                        let pat_desc = format!("{:?}", child_pat.pattern_type);
1763                        ctxt.record_error(format!(
1764                            "Interleave pattern '{}' did not match any child at '{}'",
1765                            pat_desc,
1766                            ctxt.current_path()
1767                        ));
1768                        valid = false;
1769                    }
1770                }
1771            }
1772        }
1773
1774        valid
1775    }
1776}
1777
1778/// Validate a zeroOrMore pattern.
1779///
1780/// # SAFETY
1781///
1782/// - `node` must be a valid pointer to an _xmlNode or NULL.
1783fn rng_validate_zero_or_more(
1784    pattern: &RelaxNgPattern,
1785    node: *mut _xmlNode,
1786    schema: &RelaxNgSchema,
1787    ctxt: &mut RelaxNgValidCtxt,
1788) -> bool {
1789    unsafe {
1790        if node.is_null() || pattern.children.is_empty() {
1791            return true;
1792        }
1793
1794        let child_pat = &pattern.children[0];
1795        let valid = true;
1796
1797        // Match as many child nodes as possible against the pattern
1798        let mut child = (*node).children;
1799        while !child.is_null() {
1800            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1801                let saved_errors = ctxt.errors.len();
1802                let saved_nb = ctxt.nb_errors;
1803
1804                if !rng_validate_pattern(child_pat, child, schema, ctxt) {
1805                    // This child doesn't match the zeroOrMore pattern
1806                    // Restore errors and stop
1807                    ctxt.errors.truncate(saved_errors);
1808                    ctxt.nb_errors = saved_nb;
1809                    break;
1810                }
1811                // Successfully matched — errors from matching are valid
1812            }
1813            child = (*child).next;
1814        }
1815
1816        valid
1817    }
1818}
1819
1820/// Validate a oneOrMore pattern.
1821///
1822/// # SAFETY
1823///
1824/// - `node` must be a valid pointer to an _xmlNode or NULL.
1825fn rng_validate_one_or_more(
1826    pattern: &RelaxNgPattern,
1827    node: *mut _xmlNode,
1828    schema: &RelaxNgSchema,
1829    ctxt: &mut RelaxNgValidCtxt,
1830) -> bool {
1831    unsafe {
1832        if node.is_null() || pattern.children.is_empty() {
1833            ctxt.record_error(format!(
1834                "Expected at least one matching element for oneOrMore at '{}'",
1835                ctxt.current_path()
1836            ));
1837            return false;
1838        }
1839
1840        let child_pat = &pattern.children[0];
1841        let mut matched = false;
1842        let mut valid = true;
1843
1844        // Match at least one child, then as many as possible
1845        let mut child = (*node).children;
1846        while !child.is_null() {
1847            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1848                let saved_errors = ctxt.errors.len();
1849                let saved_nb = ctxt.nb_errors;
1850
1851                if rng_validate_pattern(child_pat, child, schema, ctxt) {
1852                    matched = true;
1853                } else {
1854                    // Restore errors and stop
1855                    ctxt.errors.truncate(saved_errors);
1856                    ctxt.nb_errors = saved_nb;
1857                    break;
1858                }
1859            }
1860            child = (*child).next;
1861        }
1862
1863        if !matched {
1864            ctxt.record_error(format!(
1865                "Expected at least one matching element for oneOrMore at '{}'",
1866                ctxt.current_path()
1867            ));
1868            valid = false;
1869        }
1870
1871        valid
1872    }
1873}
1874
1875/// Validate an optional pattern.
1876///
1877/// # SAFETY
1878///
1879/// - `node` must be a valid pointer to an _xmlNode or NULL.
1880fn rng_validate_optional_pattern(
1881    pattern: &RelaxNgPattern,
1882    node: *mut _xmlNode,
1883    schema: &RelaxNgSchema,
1884    ctxt: &mut RelaxNgValidCtxt,
1885) -> bool {
1886    {
1887        if pattern.children.is_empty() {
1888            return true;
1889        }
1890
1891        // Optional means the pattern can match or not — both are valid
1892        let child_pat = &pattern.children[0];
1893        let saved_errors = ctxt.errors.len();
1894        let saved_nb = ctxt.nb_errors;
1895
1896        let result = rng_validate_pattern(child_pat, node, schema, ctxt);
1897
1898        if !result {
1899            // Restore errors — it's okay that optional didn't match
1900            ctxt.errors.truncate(saved_errors);
1901            ctxt.nb_errors = saved_nb;
1902        }
1903
1904        true // Optional always returns true
1905    }
1906}
1907
1908/// Validate a list pattern.
1909///
1910/// # SAFETY
1911///
1912/// - `node` must be a valid pointer to an _xmlNode or NULL.
1913fn rng_validate_list_pattern(
1914    pattern: &RelaxNgPattern,
1915    node: *mut _xmlNode,
1916    _schema: &RelaxNgSchema,
1917    _ctxt: &mut RelaxNgValidCtxt,
1918) -> bool {
1919    unsafe {
1920        if node.is_null() {
1921            return pattern.children.is_empty();
1922        }
1923
1924        // List pattern: whitespace-separated tokens, each matching the child pattern
1925        let text = get_node_text(node);
1926        if text.trim().is_empty() {
1927            return true;
1928        }
1929
1930        let tokens: Vec<&str> = text.split_whitespace().collect();
1931        let mut valid = true;
1932
1933        for token in &tokens {
1934            // For each token, validate against the child pattern
1935            // We do a simplified check — just ensure it's non-empty
1936            if token.is_empty() {
1937                valid = false;
1938                break;
1939            }
1940        }
1941
1942        valid
1943    }
1944}
1945
1946/// Validate a group pattern.
1947///
1948/// # SAFETY
1949///
1950/// - `node` must be a valid pointer to an _xmlNode or NULL.
1951fn rng_validate_group_pattern(
1952    pattern: &RelaxNgPattern,
1953    node: *mut _xmlNode,
1954    schema: &RelaxNgSchema,
1955    ctxt: &mut RelaxNgValidCtxt,
1956) -> bool {
1957    {
1958        // Group is similar to sequence — validate children in order
1959        rng_validate_sequence_pattern(pattern, node, schema, ctxt)
1960    }
1961}
1962
1963/// Validate a data pattern.
1964///
1965/// # SAFETY
1966///
1967/// - `node` must be a valid pointer to an _xmlNode or NULL.
1968fn rng_validate_data_pattern(
1969    pattern: &RelaxNgPattern,
1970    node: *mut _xmlNode,
1971    ctxt: &mut RelaxNgValidCtxt,
1972) -> bool {
1973    unsafe {
1974        if node.is_null() {
1975            return false;
1976        }
1977
1978        let text = get_node_text(node);
1979        let datatype = pattern.datatype.as_deref();
1980
1981        if !rng_validate_datatype_value(datatype, &text) {
1982            ctxt.record_error(format!(
1983                "Value '{}' does not match datatype '{:?}' at '{}'",
1984                text,
1985                datatype,
1986                ctxt.current_path()
1987            ));
1988            return false;
1989        }
1990
1991        true
1992    }
1993}
1994
1995/// Validate a value pattern.
1996///
1997/// # SAFETY
1998///
1999/// - `node` must be a valid pointer to an _xmlNode or NULL.
2000fn rng_validate_value_pattern(
2001    pattern: &RelaxNgPattern,
2002    node: *mut _xmlNode,
2003    ctxt: &mut RelaxNgValidCtxt,
2004) -> bool {
2005    unsafe {
2006        if node.is_null() {
2007            return false;
2008        }
2009
2010        let text = get_node_text(node).trim().to_string();
2011        let expected = pattern.value.as_deref().unwrap_or("");
2012
2013        if text != expected {
2014            ctxt.record_error(format!(
2015                "Value '{}' does not match expected value '{}' at '{}'",
2016                text,
2017                expected,
2018                ctxt.current_path()
2019            ));
2020            return false;
2021        }
2022
2023        true
2024    }
2025}
2026
2027/// Validate a ref pattern.
2028///
2029/// # SAFETY
2030///
2031/// - `node` must be a valid pointer to an _xmlNode or NULL.
2032fn rng_validate_ref_pattern(
2033    pattern: &RelaxNgPattern,
2034    node: *mut _xmlNode,
2035    schema: &RelaxNgSchema,
2036    ctxt: &mut RelaxNgValidCtxt,
2037) -> bool {
2038    {
2039        let ref_name = pattern.name.as_deref().unwrap_or("");
2040
2041        if ref_name.is_empty() {
2042            ctxt.record_error("Ref pattern has no name".to_string());
2043            return false;
2044        }
2045
2046        // Look up the definition
2047        match schema.grammar.lookup(ref_name) {
2048            Some(def_pattern) => {
2049                // Validate against the definition's pattern
2050                rng_validate_pattern(def_pattern, node, schema, ctxt)
2051            }
2052            None => {
2053                ctxt.record_error(format!(
2054                    "Undefined reference '{}' at '{}'",
2055                    ref_name,
2056                    ctxt.current_path()
2057                ));
2058                false
2059            }
2060        }
2061    }
2062}
2063
2064// ═══════════════════════════════════════════════════════════════════════════════
2065// Datatype Validation for RELAX NG
2066// ═══════════════════════════════════════════════════════════════════════════════
2067
2068/// Validate a value against a RELAX NG datatype.
2069///
2070/// RELAX NG supports a subset of XML Schema datatypes. This function
2071/// provides basic datatype validation for common types.
2072fn rng_validate_datatype_value(datatype: Option<&str>, value: &str) -> bool {
2073    let dt = match datatype {
2074        Some(d) => d,
2075        None => return true, // No datatype specified — accept anything
2076    };
2077
2078    match dt {
2079        "string" | "token" => true,
2080        "boolean" => {
2081            matches!(value, "true" | "false" | "1" | "0")
2082        }
2083        "integer" | "int" | "short" | "byte" | "long" => {
2084            if value.is_empty() {
2085                return false;
2086            }
2087            let trimmed = if value.starts_with('+') || value.starts_with('-') {
2088                &value[1..]
2089            } else {
2090                value
2091            };
2092            !trimmed.is_empty() && trimmed.chars().all(|c| c.is_ascii_digit())
2093        }
2094        "decimal" | "double" | "float" => {
2095            if value.is_empty() {
2096                return false;
2097            }
2098            // Allow INF, -INF, NaN for float/double
2099            if matches!(dt, "float" | "double") && matches!(value, "INF" | "-INF" | "NaN") {
2100                return true;
2101            }
2102            value.parse::<f64>().is_ok()
2103        }
2104        "NCName" | "Name" | "ID" | "IDREF" | "NMTOKEN" => {
2105            !value.is_empty() && !value.starts_with(|c: char| c.is_ascii_digit())
2106        }
2107        "anyURI" => {
2108            // Simple URI validation — non-empty and no spaces
2109            !value.is_empty() && !value.contains(char::is_whitespace)
2110        }
2111        "QName" => {
2112            if value.is_empty() {
2113                return false;
2114            }
2115            if let Some(pos) = value.find(':') {
2116                pos > 0 && pos < value.len() - 1
2117            } else {
2118                true
2119            }
2120        }
2121        _ => {
2122            // Unknown datatype — accept by default
2123            // This matches libxml2's lenient behavior
2124            true
2125        }
2126    }
2127}
2128
2129// ═══════════════════════════════════════════════════════════════════════════════
2130// Public API Functions
2131// ═══════════════════════════════════════════════════════════════════════════════
2132
2133/// Parse a RELAX NG schema from an XML string.
2134///
2135/// Returns the parsed schema, or an error message on failure.
2136pub fn rng_parse_schema(xml_doc: &str) -> Result<RelaxNgSchema, String> {
2137    rng_parse(xml_doc)
2138}
2139
2140/// Parse a RELAX NG schema from a parsed XML document.
2141///
2142/// # SAFETY
2143///
2144/// - `doc` must be a valid pointer to an _xmlDoc.
2145pub unsafe fn rng_parse_schema_doc(doc: *mut _xmlDoc) -> Result<RelaxNgSchema, String> {
2146    rng_parse_doc(doc)
2147}
2148
2149/// Validate a document against a RELAX NG schema.
2150///
2151/// Returns `true` if the document is valid.
2152///
2153/// # SAFETY
2154///
2155/// - `doc` must be a valid pointer to an _xmlDoc or NULL.
2156pub unsafe fn rng_validate_doc_schema(
2157    schema: &RelaxNgSchema,
2158    doc: *mut _xmlDoc,
2159    ctxt: &mut RelaxNgValidCtxt,
2160) -> bool {
2161    rng_validate_doc(schema, doc, ctxt)
2162}
2163
2164// ═══════════════════════════════════════════════════════════════════════════════
2165// C ABI Functions
2166// ═══════════════════════════════════════════════════════════════════════════════
2167
2168// These are the C-compatible entry points that get exported via the ABI layer.
2169// They use raw pointers and follow libxml2's calling conventions.
2170
2171/// Create a new RELAX NG parser context.
2172///
2173/// # UPSTREAM-PARITY
2174///
2175/// ```c
2176/// xmlRelaxNGParserCtxtPtr xmlRelaxNGNewParserCtxt(const char *URL);
2177/// ```
2178///
2179/// # SAFETY
2180///
2181/// - `url` must be a valid null-terminated C string or NULL.
2182#[no_mangle]
2183pub unsafe extern "C" fn xmlRelaxNGNewParserCtxt(url: *const c_char) -> *mut c_void {
2184    if url.is_null() {
2185        let ctxt = allocator::xmlMallocZero(size_of::<RelaxNgSchema>() as usize);
2186        return ctxt;
2187    }
2188
2189    let url_str = unsafe {
2190        let mut len = 0;
2191        while *url.add(len) != 0 {
2192            len += 1;
2193        }
2194        let slice = std::slice::from_raw_parts(url as *const u8, len);
2195        String::from_utf8_lossy(slice).to_string()
2196    };
2197
2198    // Try to parse the schema from the URL
2199    if !url_str.is_empty() {
2200        let url_c = std::ffi::CString::new(url_str.clone()).ok();
2201        if let Some(c) = url_c {
2202            let doc = crate::abi::exports_xml2::xmlParseFile(c.as_ptr());
2203            if !doc.is_null() {
2204                let result = rng_parse_doc(doc);
2205                crate::abi::exports_xml2::xmlFreeDoc(doc);
2206                if let Ok(schema) = result {
2207                    let schema_box = Box::new(schema);
2208                    return Box::into_raw(schema_box) as *mut c_void;
2209                }
2210            }
2211        }
2212    }
2213
2214    // Return empty context for later parsing
2215
2216    allocator::xmlMallocZero(size_of::<RelaxNgSchema>() as usize)
2217}
2218
2219/// Create a new RELAX NG parser context from a memory buffer.
2220///
2221/// # UPSTREAM-PARITY
2222///
2223/// ```c
2224/// xmlRelaxNGParserCtxtPtr xmlRelaxNGNewMemParserCtxt(const char *buffer, int size);
2225/// ```
2226///
2227/// # SAFETY
2228///
2229/// - `buffer` must be a valid pointer to a buffer of at least `size` bytes.
2230#[no_mangle]
2231pub unsafe extern "C" fn xmlRelaxNGNewMemParserCtxt(
2232    buffer: *const c_char,
2233    size: c_int,
2234) -> *mut c_void {
2235    if buffer.is_null() || size <= 0 {
2236        return ptr::null_mut();
2237    }
2238
2239    // Parse the schema immediately
2240    let buf_slice = unsafe { std::slice::from_raw_parts(buffer as *const u8, size as usize) };
2241    let xml_str = String::from_utf8_lossy(buf_slice).to_string();
2242
2243    match rng_parse(&xml_str) {
2244        Ok(schema) => {
2245            let schema_box = Box::new(schema);
2246            Box::into_raw(schema_box) as *mut c_void
2247        }
2248        Err(_) => ptr::null_mut(),
2249    }
2250}
2251
2252/// Parse a RELAX NG schema.
2253///
2254/// # UPSTREAM-PARITY
2255///
2256/// ```c
2257/// xmlRelaxNGPtr xmlRelaxNGParse(xmlRelaxNGParserCtxtPtr ctxt);
2258/// ```
2259///
2260/// # SAFETY
2261///
2262/// - `ctxt` must be a valid pointer to a parser context, or NULL.
2263#[no_mangle]
2264pub const unsafe extern "C" fn xmlRelaxNGParse(ctxt: *mut c_void) -> *mut c_void {
2265    if ctxt.is_null() {
2266        return ptr::null_mut();
2267    }
2268
2269    // If the context already contains a parsed schema (from xmlRelaxNGNewMemParserCtxt),
2270    // return it. Otherwise, return the context as-is.
2271    ctxt
2272}
2273
2274/// Free a RELAX NG schema.
2275///
2276/// # UPSTREAM-PARITY
2277///
2278/// ```c
2279/// void xmlRelaxNGFree(xmlRelaxNGPtr schema);
2280/// ```
2281///
2282/// # SAFETY
2283///
2284/// - `schema` must be a valid pointer to a schema, or NULL.
2285#[no_mangle]
2286pub unsafe extern "C" fn xmlRelaxNGFree(schema: *mut c_void) {
2287    if schema.is_null() {
2288        return;
2289    }
2290    // SAFETY: Reconstruct the Box to drop it.
2291    unsafe {
2292        let _ = Box::from_raw(schema as *mut RelaxNgSchema);
2293    }
2294}
2295
2296/// Free a RELAX NG parser context.
2297///
2298/// # UPSTREAM-PARITY
2299///
2300/// ```c
2301/// void xmlRelaxNGFreeParserCtxt(xmlRelaxNGParserCtxtPtr ctxt);
2302/// ```
2303///
2304/// # SAFETY
2305///
2306/// - `ctxt` must be a valid pointer to a parser context, or NULL.
2307#[no_mangle]
2308pub unsafe extern "C" fn xmlRelaxNGFreeParserCtxt(ctxt: *mut c_void) {
2309    if ctxt.is_null() {
2310        return;
2311    }
2312    // SAFETY: Reconstruct the Box to drop it.
2313    unsafe {
2314        let _ = Box::from_raw(ctxt as *mut RelaxNgSchema);
2315    }
2316}
2317
2318/// Create a new RELAX NG validation context.
2319///
2320/// # UPSTREAM-PARITY
2321///
2322/// ```c
2323/// xmlRelaxNGValidCtxtPtr xmlRelaxNGNewValidCtxt(xmlRelaxNGPtr schema);
2324/// ```
2325///
2326/// # SAFETY
2327///
2328/// - `schema` must be a valid pointer to a schema, or NULL.
2329#[no_mangle]
2330pub unsafe extern "C" fn xmlRelaxNGNewValidCtxt(schema: *mut c_void) -> *mut c_void {
2331    let mut ctxt = RelaxNgValidCtxt::new();
2332
2333    if !schema.is_null() {
2334        // SAFETY: The schema pointer is assumed to be a valid RelaxNgSchema.
2335        unsafe {
2336            let schema_ref = &*(schema as *const RelaxNgSchema);
2337            ctxt.schema = Some(schema_ref.clone());
2338        }
2339    }
2340
2341    let boxed = Box::new(ctxt);
2342    Box::into_raw(boxed) as *mut c_void
2343}
2344
2345/// Free a RELAX NG validation context.
2346///
2347/// # UPSTREAM-PARITY
2348///
2349/// ```c
2350/// void xmlRelaxNGFreeValidCtxt(xmlRelaxNGValidCtxtPtr ctxt);
2351/// ```
2352///
2353/// # SAFETY
2354///
2355/// - `ctxt` must be a valid pointer to a validation context, or NULL.
2356#[no_mangle]
2357pub unsafe extern "C" fn xmlRelaxNGFreeValidCtxt(ctxt: *mut c_void) {
2358    if ctxt.is_null() {
2359        return;
2360    }
2361    // SAFETY: Reconstruct the Box to drop it.
2362    unsafe {
2363        let _ = Box::from_raw(ctxt as *mut RelaxNgValidCtxt);
2364    }
2365}
2366
2367/// Validate a document against a RELAX NG schema.
2368///
2369/// # UPSTREAM-PARITY
2370///
2371/// ```c
2372/// int xmlRelaxNGValidateDoc(xmlRelaxNGValidCtxtPtr ctxt, xmlDocPtr doc);
2373/// ```
2374///
2375/// Returns 0 if valid, -1 on internal error, or the number of validation errors.
2376///
2377/// # SAFETY
2378///
2379/// - `ctxt` must be a valid pointer to a validation context, or NULL.
2380/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
2381#[no_mangle]
2382pub unsafe extern "C" fn xmlRelaxNGValidateDoc(ctxt: *mut c_void, doc: *mut _xmlDoc) -> c_int {
2383    if ctxt.is_null() || doc.is_null() {
2384        return -1;
2385    }
2386
2387    unsafe {
2388        let valid_ctxt = &mut *(ctxt as *mut RelaxNgValidCtxt);
2389        let schema = match &valid_ctxt.schema {
2390            Some(s) => s,
2391            None => return -1,
2392        };
2393
2394        let mut temp_ctxt = RelaxNgValidCtxt::new();
2395
2396        let valid = rng_validate_doc(schema, doc, &mut temp_ctxt);
2397
2398        if valid {
2399            0
2400        } else {
2401            valid_ctxt.errors = temp_ctxt.errors;
2402            valid_ctxt.nb_errors = temp_ctxt.nb_errors;
2403            temp_ctxt.nb_errors
2404        }
2405    }
2406}
2407
2408/// Validate a full element against a RELAX NG schema.
2409///
2410/// # UPSTREAM-PARITY
2411///
2412/// ```c
2413/// int xmlRelaxNGValidateFullElement(xmlRelaxNGValidCtxtPtr ctxt,
2414///                                    xmlDocPtr doc,
2415///                                    xmlNodePtr elem);
2416/// ```
2417///
2418/// Returns 0 if valid, -1 on internal error, or the number of validation errors.
2419///
2420/// # SAFETY
2421///
2422/// - `ctxt` must be a valid pointer to a validation context, or NULL.
2423/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
2424/// - `elem` must be a valid pointer to an element node, or NULL.
2425#[no_mangle]
2426pub unsafe extern "C" fn xmlRelaxNGValidateFullElement(
2427    ctxt: *mut c_void,
2428    doc: *mut _xmlDoc,
2429    elem: *mut _xmlNode,
2430) -> c_int {
2431    if ctxt.is_null() || doc.is_null() || elem.is_null() {
2432        return -1;
2433    }
2434
2435    unsafe {
2436        let valid_ctxt = &mut *(ctxt as *mut RelaxNgValidCtxt);
2437        let schema = match &valid_ctxt.schema {
2438            Some(s) => s,
2439            None => return -1,
2440        };
2441
2442        let mut temp_ctxt = RelaxNgValidCtxt::new();
2443        temp_ctxt.path = valid_ctxt.path.clone();
2444
2445        let start_pattern = match &schema.grammar.start {
2446            Some(p) => p,
2447            None => return -1,
2448        };
2449
2450        let valid = rng_validate_pattern(start_pattern, elem, schema, &mut temp_ctxt);
2451
2452        if valid {
2453            0
2454        } else {
2455            valid_ctxt.errors = temp_ctxt.errors;
2456            valid_ctxt.nb_errors = temp_ctxt.nb_errors;
2457            temp_ctxt.nb_errors
2458        }
2459    }
2460}
2461
2462// ═══════════════════════════════════════════════════════════════════════════════
2463// Tests
2464// ═══════════════════════════════════════════════════════════════════════════════
2465
2466#[cfg(test)]
2467mod tests {
2468    use super::*;
2469
2470    // ── Name Class Tests ───────────────────────────────────────────────────
2471
2472    #[test]
2473    fn test_name_class_name() {
2474        let nc = RelaxNgNameClass::Name("foo".to_string());
2475        assert!(nc.matches("foo", None));
2476        assert!(!nc.matches("bar", None));
2477        assert!(!nc.matches("FOO", None));
2478    }
2479
2480    #[test]
2481    fn test_name_class_any_name() {
2482        let nc = RelaxNgNameClass::AnyName;
2483        assert!(nc.matches("foo", None));
2484        assert!(nc.matches("bar", None));
2485        assert!(nc.matches("anything", Some("urn:ns")));
2486    }
2487
2488    #[test]
2489    fn test_name_class_ns_name() {
2490        let nc = RelaxNgNameClass::NsName("urn:example".to_string());
2491        assert!(nc.matches("foo", Some("urn:example")));
2492        assert!(!nc.matches("foo", Some("urn:other")));
2493        assert!(!nc.matches("foo", None));
2494    }
2495
2496    #[test]
2497    fn test_name_class_choice() {
2498        let nc = RelaxNgNameClass::Choice(vec![
2499            RelaxNgNameClass::Name("a".to_string()),
2500            RelaxNgNameClass::Name("b".to_string()),
2501        ]);
2502        assert!(nc.matches("a", None));
2503        assert!(nc.matches("b", None));
2504        assert!(!nc.matches("c", None));
2505    }
2506
2507    #[test]
2508    fn test_name_class_except() {
2509        let nc = RelaxNgNameClass::Except(
2510            Box::new(RelaxNgNameClass::AnyName),
2511            Box::new(RelaxNgNameClass::Name("bad".to_string())),
2512        );
2513        assert!(nc.matches("good", None));
2514        assert!(!nc.matches("bad", None));
2515    }
2516
2517    // ── Schema Parsing Tests ──────────────────────────────────────────────
2518
2519    #[test]
2520    fn test_parse_simple_element_schema() {
2521        let schema_xml = r#"<?xml version="1.0"?>
2522<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
2523  <text/>
2524</element>"#;
2525
2526        let result = rng_parse(schema_xml);
2527        assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
2528        let schema = result.unwrap();
2529        assert!(schema.grammar.start.is_some());
2530        if let Some(ref start) = schema.grammar.start {
2531            assert_eq!(start.pattern_type, RelaxNgPatternType::Element);
2532            assert_eq!(start.name.as_deref(), Some("root"));
2533        }
2534    }
2535
2536    #[test]
2537    fn test_parse_grammar_schema() {
2538        let schema_xml = r#"<?xml version="1.0"?>
2539<grammar xmlns="http://relaxng.org/ns/structure/1.0">
2540  <start>
2541    <element name="root">
2542      <text/>
2543    </element>
2544  </start>
2545</grammar>"#;
2546
2547        let result = rng_parse(schema_xml);
2548        assert!(
2549            result.is_ok(),
2550            "Failed to parse grammar: {:?}",
2551            result.err()
2552        );
2553        let schema = result.unwrap();
2554        assert!(schema.grammar.start.is_some());
2555    }
2556
2557    #[test]
2558    fn test_parse_with_define_and_ref() {
2559        let schema_xml = r#"<?xml version="1.0"?>
2560<grammar xmlns="http://relaxng.org/ns/structure/1.0">
2561  <define name="textBlock">
2562    <text/>
2563  </define>
2564  <start>
2565    <element name="doc">
2566      <ref name="textBlock"/>
2567    </element>
2568  </start>
2569</grammar>"#;
2570
2571        let result = rng_parse(schema_xml);
2572        assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
2573        let schema = result.unwrap();
2574        assert_eq!(schema.grammar.defines.len(), 1);
2575        assert_eq!(schema.grammar.defines[0].name, "textBlock");
2576        assert!(schema.grammar.start.is_some());
2577    }
2578
2579    #[test]
2580    fn test_parse_choice_schema() {
2581        let schema_xml = r#"<?xml version="1.0"?>
2582<grammar xmlns="http://relaxng.org/ns/structure/1.0">
2583  <start>
2584    <choice>
2585      <element name="a">
2586        <text/>
2587      </element>
2588      <element name="b">
2589        <text/>
2590      </element>
2591    </choice>
2592  </start>
2593</grammar>"#;
2594
2595        let result = rng_parse(schema_xml);
2596        assert!(result.is_ok(), "Failed to parse choice: {:?}", result.err());
2597    }
2598
2599    #[test]
2600    fn test_parse_attribute_schema() {
2601        let schema_xml = r#"<?xml version="1.0"?>
2602<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
2603  <attribute name="attr1">
2604    <text/>
2605  </attribute>
2606  <text/>
2607</element>"#;
2608
2609        let result = rng_parse(schema_xml);
2610        assert!(
2611            result.is_ok(),
2612            "Failed to parse attribute: {:?}",
2613            result.err()
2614        );
2615    }
2616
2617    #[test]
2618    fn test_parse_empty_document_fails() {
2619        let result = rng_parse("");
2620        assert!(result.is_err());
2621    }
2622
2623    #[test]
2624    fn test_parse_invalid_xml_fails() {
2625        let result = rng_parse("not valid xml <<<");
2626        assert!(result.is_err());
2627    }
2628
2629    // ── Validation Tests ──────────────────────────────────────────────────
2630
2631    #[test]
2632    fn test_validate_simple_element() {
2633        let schema_xml = r#"<?xml version="1.0"?>
2634<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
2635  <text/>
2636</element>"#;
2637
2638        let doc_xml = r#"<?xml version="1.0"?>
2639<root>Hello</root>"#;
2640
2641        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
2642
2643        let doc = unsafe {
2644            crate::abi::exports_xml2::xmlReadMemory(
2645                doc_xml.as_ptr() as *const c_char,
2646                doc_xml.len() as c_int,
2647                c"test.xml".as_ptr() as *const c_char,
2648                ptr::null(),
2649                0,
2650            )
2651        };
2652        assert!(!doc.is_null(), "Failed to parse document");
2653
2654        let mut ctxt = RelaxNgValidCtxt::new();
2655        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
2656        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2657
2658        assert!(valid, "Validation failed: {:?}", ctxt.errors);
2659    }
2660
2661    #[test]
2662    fn test_validate_element_mismatch() {
2663        let schema_xml = r#"<?xml version="1.0"?>
2664<element name="expected" xmlns="http://relaxng.org/ns/structure/1.0">
2665  <text/>
2666</element>"#;
2667
2668        let doc_xml = r#"<?xml version="1.0"?>
2669<actual>Content</actual>"#;
2670
2671        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
2672
2673        let doc = unsafe {
2674            crate::abi::exports_xml2::xmlReadMemory(
2675                doc_xml.as_ptr() as *const c_char,
2676                doc_xml.len() as c_int,
2677                c"test.xml".as_ptr() as *const c_char,
2678                ptr::null(),
2679                0,
2680            )
2681        };
2682        assert!(!doc.is_null());
2683
2684        let mut ctxt = RelaxNgValidCtxt::new();
2685        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
2686        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2687
2688        assert!(!valid, "Validation should have failed");
2689        assert!(ctxt.nb_errors > 0);
2690    }
2691
2692    #[test]
2693    fn test_validate_with_attribute() {
2694        let schema_xml = r#"<?xml version="1.0"?>
2695<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
2696  <attribute name="id">
2697    <text/>
2698  </attribute>
2699  <text/>
2700</element>"#;
2701
2702        let doc_xml = r#"<?xml version="1.0"?>
2703<root id="x1">Content</root>"#;
2704
2705        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
2706
2707        let doc = unsafe {
2708            crate::abi::exports_xml2::xmlReadMemory(
2709                doc_xml.as_ptr() as *const c_char,
2710                doc_xml.len() as c_int,
2711                c"test.xml".as_ptr() as *const c_char,
2712                ptr::null(),
2713                0,
2714            )
2715        };
2716        assert!(!doc.is_null());
2717
2718        let mut ctxt = RelaxNgValidCtxt::new();
2719        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
2720        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2721
2722        assert!(valid, "Validation failed: {:?}", ctxt.errors);
2723    }
2724
2725    #[test]
2726    fn test_validate_missing_attribute() {
2727        let schema_xml = r#"<?xml version="1.0"?>
2728<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
2729  <attribute name="required">
2730    <text/>
2731  </attribute>
2732  <text/>
2733</element>"#;
2734
2735        let doc_xml = r#"<?xml version="1.0"?>
2736<root>Content</root>"#;
2737
2738        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
2739
2740        let doc = unsafe {
2741            crate::abi::exports_xml2::xmlReadMemory(
2742                doc_xml.as_ptr() as *const c_char,
2743                doc_xml.len() as c_int,
2744                c"test.xml".as_ptr() as *const c_char,
2745                ptr::null(),
2746                0,
2747            )
2748        };
2749        assert!(!doc.is_null());
2750
2751        let mut ctxt = RelaxNgValidCtxt::new();
2752        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
2753        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2754
2755        assert!(
2756            !valid,
2757            "Validation should have failed for missing attribute"
2758        );
2759    }
2760
2761    #[test]
2762    fn test_validate_with_choice() {
2763        let schema_xml = r#"<?xml version="1.0"?>
2764<grammar xmlns="http://relaxng.org/ns/structure/1.0">
2765  <start>
2766    <choice>
2767      <element name="a">
2768        <text/>
2769      </element>
2770      <element name="b">
2771        <text/>
2772      </element>
2773    </choice>
2774  </start>
2775</grammar>"#;
2776
2777        let doc_xml = r#"<?xml version="1.0"?>
2778<a>First choice</a>"#;
2779
2780        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
2781
2782        let doc = unsafe {
2783            crate::abi::exports_xml2::xmlReadMemory(
2784                doc_xml.as_ptr() as *const c_char,
2785                doc_xml.len() as c_int,
2786                c"test.xml".as_ptr() as *const c_char,
2787                ptr::null(),
2788                0,
2789            )
2790        };
2791        assert!(!doc.is_null());
2792
2793        let mut ctxt = RelaxNgValidCtxt::new();
2794        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
2795        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2796
2797        assert!(valid, "Choice validation failed: {:?}", ctxt.errors);
2798    }
2799
2800    #[test]
2801    fn test_validate_choice_no_match() {
2802        let schema_xml = r#"<?xml version="1.0"?>
2803<grammar xmlns="http://relaxng.org/ns/structure/1.0">
2804  <start>
2805    <choice>
2806      <element name="a">
2807        <text/>
2808      </element>
2809      <element name="b">
2810        <text/>
2811      </element>
2812    </choice>
2813  </start>
2814</grammar>"#;
2815
2816        let doc_xml = r#"<?xml version="1.0"?>
2817<c>Neither choice</c>"#;
2818
2819        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
2820
2821        let doc = unsafe {
2822            crate::abi::exports_xml2::xmlReadMemory(
2823                doc_xml.as_ptr() as *const c_char,
2824                doc_xml.len() as c_int,
2825                c"test.xml".as_ptr() as *const c_char,
2826                ptr::null(),
2827                0,
2828            )
2829        };
2830        assert!(!doc.is_null());
2831
2832        let mut ctxt = RelaxNgValidCtxt::new();
2833        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
2834        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2835
2836        assert!(
2837            !valid,
2838            "Validation should have failed for no matching choice"
2839        );
2840    }
2841
2842    #[test]
2843    fn test_validate_grammar_with_ref() {
2844        let schema_xml = r#"<?xml version="1.0"?>
2845<grammar xmlns="http://relaxng.org/ns/structure/1.0">
2846  <define name="para">
2847    <element name="p">
2848      <text/>
2849    </element>
2850  </define>
2851  <start>
2852    <element name="doc">
2853      <zeroOrMore>
2854        <ref name="para"/>
2855      </zeroOrMore>
2856    </element>
2857  </start>
2858</grammar>"#;
2859
2860        let doc_xml = r#"<?xml version="1.0"?>
2861<doc><p>First</p><p>Second</p></doc>"#;
2862
2863        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
2864
2865        let doc = unsafe {
2866            crate::abi::exports_xml2::xmlReadMemory(
2867                doc_xml.as_ptr() as *const c_char,
2868                doc_xml.len() as c_int,
2869                c"test.xml".as_ptr() as *const c_char,
2870                ptr::null(),
2871                0,
2872            )
2873        };
2874        assert!(!doc.is_null());
2875
2876        let mut ctxt = RelaxNgValidCtxt::new();
2877        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
2878        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2879
2880        assert!(valid, "Ref validation failed: {:?}", ctxt.errors);
2881    }
2882
2883    #[test]
2884    fn test_validate_zero_or_more() {
2885        let schema_xml = r#"<?xml version="1.0"?>
2886<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
2887  <zeroOrMore>
2888    <element name="item">
2889      <text/>
2890    </element>
2891  </zeroOrMore>
2892</element>"#;
2893
2894        let doc_xml = r#"<?xml version="1.0"?>
2895<root><item>A</item><item>B</item></root>"#;
2896
2897        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
2898
2899        let doc = unsafe {
2900            crate::abi::exports_xml2::xmlReadMemory(
2901                doc_xml.as_ptr() as *const c_char,
2902                doc_xml.len() as c_int,
2903                c"test.xml".as_ptr() as *const c_char,
2904                ptr::null(),
2905                0,
2906            )
2907        };
2908        assert!(!doc.is_null());
2909
2910        let mut ctxt = RelaxNgValidCtxt::new();
2911        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
2912        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2913
2914        assert!(valid, "zeroOrMore validation failed: {:?}", ctxt.errors);
2915    }
2916
2917    #[test]
2918    fn test_validate_zero_or_more_empty() {
2919        let schema_xml = r#"<?xml version="1.0"?>
2920<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
2921  <zeroOrMore>
2922    <element name="item">
2923      <text/>
2924    </element>
2925  </zeroOrMore>
2926</element>"#;
2927
2928        let doc_xml = r#"<?xml version="1.0"?>
2929<root></root>"#;
2930
2931        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
2932
2933        let doc = unsafe {
2934            crate::abi::exports_xml2::xmlReadMemory(
2935                doc_xml.as_ptr() as *const c_char,
2936                doc_xml.len() as c_int,
2937                c"test.xml".as_ptr() as *const c_char,
2938                ptr::null(),
2939                0,
2940            )
2941        };
2942        assert!(!doc.is_null());
2943
2944        let mut ctxt = RelaxNgValidCtxt::new();
2945        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
2946        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2947
2948        assert!(valid, "Empty zeroOrMore should be valid");
2949    }
2950
2951    #[test]
2952    fn test_validate_one_or_more() {
2953        let schema_xml = r#"<?xml version="1.0"?>
2954<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
2955  <oneOrMore>
2956    <element name="item">
2957      <text/>
2958    </element>
2959  </oneOrMore>
2960</element>"#;
2961
2962        let doc_xml = r#"<?xml version="1.0"?>
2963<root><item>Single</item></root>"#;
2964
2965        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
2966
2967        let doc = unsafe {
2968            crate::abi::exports_xml2::xmlReadMemory(
2969                doc_xml.as_ptr() as *const c_char,
2970                doc_xml.len() as c_int,
2971                c"test.xml".as_ptr() as *const c_char,
2972                ptr::null(),
2973                0,
2974            )
2975        };
2976        assert!(!doc.is_null());
2977
2978        let mut ctxt = RelaxNgValidCtxt::new();
2979        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
2980        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2981
2982        assert!(valid, "oneOrMore validation failed: {:?}", ctxt.errors);
2983    }
2984
2985    #[test]
2986    fn test_validate_optional_present() {
2987        let schema_xml = r#"<?xml version="1.0"?>
2988<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
2989  <optional>
2990    <element name="opt">
2991      <text/>
2992    </element>
2993  </optional>
2994  <text/>
2995</element>"#;
2996
2997        let doc_xml = r#"<?xml version="1.0"?>
2998<root><opt>present</opt>text</root>"#;
2999
3000        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3001
3002        let doc = unsafe {
3003            crate::abi::exports_xml2::xmlReadMemory(
3004                doc_xml.as_ptr() as *const c_char,
3005                doc_xml.len() as c_int,
3006                c"test.xml".as_ptr() as *const c_char,
3007                ptr::null(),
3008                0,
3009            )
3010        };
3011        assert!(!doc.is_null());
3012
3013        let mut ctxt = RelaxNgValidCtxt::new();
3014        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3015        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3016
3017        assert!(
3018            valid,
3019            "Optional present validation failed: {:?}",
3020            ctxt.errors
3021        );
3022    }
3023
3024    #[test]
3025    fn test_validate_optional_absent() {
3026        let schema_xml = r#"<?xml version="1.0"?>
3027<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
3028  <optional>
3029    <element name="opt">
3030      <text/>
3031    </element>
3032  </optional>
3033  <text/>
3034</element>"#;
3035
3036        let doc_xml = r#"<?xml version="1.0"?>
3037<root>text only</root>"#;
3038
3039        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3040
3041        let doc = unsafe {
3042            crate::abi::exports_xml2::xmlReadMemory(
3043                doc_xml.as_ptr() as *const c_char,
3044                doc_xml.len() as c_int,
3045                c"test.xml".as_ptr() as *const c_char,
3046                ptr::null(),
3047                0,
3048            )
3049        };
3050        assert!(!doc.is_null());
3051
3052        let mut ctxt = RelaxNgValidCtxt::new();
3053        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3054        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3055
3056        assert!(
3057            valid,
3058            "Optional absent validation failed: {:?}",
3059            ctxt.errors
3060        );
3061    }
3062
3063    #[test]
3064    fn test_validate_sequence() {
3065        let schema_xml = r#"<?xml version="1.0"?>
3066<grammar xmlns="http://relaxng.org/ns/structure/1.0">
3067  <start>
3068    <element name="root">
3069      <sequence>
3070        <element name="first">
3071          <text/>
3072        </element>
3073        <element name="second">
3074          <text/>
3075        </element>
3076      </sequence>
3077    </element>
3078  </start>
3079</grammar>"#;
3080
3081        let doc_xml = r#"<?xml version="1.0"?>
3082<root><first>First</first><second>Second</second></root>"#;
3083
3084        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3085
3086        let doc = unsafe {
3087            crate::abi::exports_xml2::xmlReadMemory(
3088                doc_xml.as_ptr() as *const c_char,
3089                doc_xml.len() as c_int,
3090                c"test.xml".as_ptr() as *const c_char,
3091                ptr::null(),
3092                0,
3093            )
3094        };
3095        assert!(!doc.is_null());
3096
3097        let mut ctxt = RelaxNgValidCtxt::new();
3098        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3099        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3100
3101        assert!(valid, "Sequence validation failed: {:?}", ctxt.errors);
3102    }
3103
3104    #[test]
3105    fn test_validate_data_pattern() {
3106        let schema_xml = r#"<?xml version="1.0"?>
3107<element name="age" xmlns="http://relaxng.org/ns/structure/1.0">
3108  <data type="integer"/>
3109</element>"#;
3110
3111        let doc_xml = r#"<?xml version="1.0"?>
3112<age>25</age>"#;
3113
3114        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3115
3116        let doc = unsafe {
3117            crate::abi::exports_xml2::xmlReadMemory(
3118                doc_xml.as_ptr() as *const c_char,
3119                doc_xml.len() as c_int,
3120                c"test.xml".as_ptr() as *const c_char,
3121                ptr::null(),
3122                0,
3123            )
3124        };
3125        assert!(!doc.is_null());
3126
3127        let mut ctxt = RelaxNgValidCtxt::new();
3128        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3129        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3130
3131        assert!(valid, "Data pattern validation failed: {:?}", ctxt.errors);
3132    }
3133
3134    #[test]
3135    fn test_validate_data_pattern_invalid() {
3136        let schema_xml = r#"<?xml version="1.0"?>
3137<element name="age" xmlns="http://relaxng.org/ns/structure/1.0">
3138  <data type="integer"/>
3139</element>"#;
3140
3141        let doc_xml = r#"<?xml version="1.0"?>
3142<age>not-a-number</age>"#;
3143
3144        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3145
3146        let doc = unsafe {
3147            crate::abi::exports_xml2::xmlReadMemory(
3148                doc_xml.as_ptr() as *const c_char,
3149                doc_xml.len() as c_int,
3150                c"test.xml".as_ptr() as *const c_char,
3151                ptr::null(),
3152                0,
3153            )
3154        };
3155        assert!(!doc.is_null());
3156
3157        let mut ctxt = RelaxNgValidCtxt::new();
3158        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3159        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3160
3161        assert!(!valid, "Validation should have failed for invalid integer");
3162    }
3163
3164    #[test]
3165    fn test_validate_value_pattern() {
3166        let schema_xml = r#"<?xml version="1.0"?>
3167<element name="status" xmlns="http://relaxng.org/ns/structure/1.0">
3168  <value>active</value>
3169</element>"#;
3170
3171        let doc_xml = r#"<?xml version="1.0"?>
3172<status>active</status>"#;
3173
3174        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3175
3176        let doc = unsafe {
3177            crate::abi::exports_xml2::xmlReadMemory(
3178                doc_xml.as_ptr() as *const c_char,
3179                doc_xml.len() as c_int,
3180                c"test.xml".as_ptr() as *const c_char,
3181                ptr::null(),
3182                0,
3183            )
3184        };
3185        assert!(!doc.is_null());
3186
3187        let mut ctxt = RelaxNgValidCtxt::new();
3188        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3189        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3190
3191        assert!(valid, "Value pattern validation failed: {:?}", ctxt.errors);
3192    }
3193
3194    #[test]
3195    fn test_validate_value_pattern_mismatch() {
3196        let schema_xml = r#"<?xml version="1.0"?>
3197<element name="status" xmlns="http://relaxng.org/ns/structure/1.0">
3198  <value>active</value>
3199</element>"#;
3200
3201        let doc_xml = r#"<?xml version="1.0"?>
3202<status>inactive</status>"#;
3203
3204        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3205
3206        let doc = unsafe {
3207            crate::abi::exports_xml2::xmlReadMemory(
3208                doc_xml.as_ptr() as *const c_char,
3209                doc_xml.len() as c_int,
3210                c"test.xml".as_ptr() as *const c_char,
3211                ptr::null(),
3212                0,
3213            )
3214        };
3215        assert!(!doc.is_null());
3216
3217        let mut ctxt = RelaxNgValidCtxt::new();
3218        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3219        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3220
3221        assert!(!valid, "Validation should have failed for value mismatch");
3222    }
3223
3224    #[test]
3225    fn test_validate_not_allowed() {
3226        let schema_xml = r#"<?xml version="1.0"?>
3227<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
3228  <notAllowed/>
3229</element>"#;
3230
3231        let doc_xml = r#"<?xml version="1.0"?>
3232<root>should not be allowed</root>"#;
3233
3234        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3235
3236        let doc = unsafe {
3237            crate::abi::exports_xml2::xmlReadMemory(
3238                doc_xml.as_ptr() as *const c_char,
3239                doc_xml.len() as c_int,
3240                c"test.xml".as_ptr() as *const c_char,
3241                ptr::null(),
3242                0,
3243            )
3244        };
3245        assert!(!doc.is_null());
3246
3247        let mut ctxt = RelaxNgValidCtxt::new();
3248        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3249        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3250
3251        assert!(!valid, "notAllowed should cause validation failure");
3252    }
3253
3254    #[test]
3255    fn test_validate_interleave() {
3256        let schema_xml = r#"<?xml version="1.0"?>
3257<grammar xmlns="http://relaxng.org/ns/structure/1.0">
3258  <start>
3259    <element name="root">
3260      <interleave>
3261        <element name="a">
3262          <text/>
3263        </element>
3264        <element name="b">
3265          <text/>
3266        </element>
3267      </interleave>
3268    </element>
3269  </start>
3270</grammar>"#;
3271
3272        let doc_xml = r#"<?xml version="1.0"?>
3273<root><a>A</a><b>B</b></root>"#;
3274
3275        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3276
3277        let doc = unsafe {
3278            crate::abi::exports_xml2::xmlReadMemory(
3279                doc_xml.as_ptr() as *const c_char,
3280                doc_xml.len() as c_int,
3281                c"test.xml".as_ptr() as *const c_char,
3282                ptr::null(),
3283                0,
3284            )
3285        };
3286        assert!(!doc.is_null());
3287
3288        let mut ctxt = RelaxNgValidCtxt::new();
3289        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3290        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3291
3292        assert!(valid, "Interleave validation failed: {:?}", ctxt.errors);
3293    }
3294
3295    #[test]
3296    fn test_validate_empty_element() {
3297        let schema_xml = r#"<?xml version="1.0"?>
3298<element name="br" xmlns="http://relaxng.org/ns/structure/1.0">
3299  <empty/>
3300</element>"#;
3301
3302        let doc_xml = r#"<?xml version="1.0"?>
3303<br/>"#;
3304
3305        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3306
3307        let doc = unsafe {
3308            crate::abi::exports_xml2::xmlReadMemory(
3309                doc_xml.as_ptr() as *const c_char,
3310                doc_xml.len() as c_int,
3311                c"test.xml".as_ptr() as *const c_char,
3312                ptr::null(),
3313                0,
3314            )
3315        };
3316        assert!(!doc.is_null());
3317
3318        let mut ctxt = RelaxNgValidCtxt::new();
3319        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3320        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3321
3322        assert!(valid, "Empty element validation failed: {:?}", ctxt.errors);
3323    }
3324
3325    // ── Datatype Validation Tests ──────────────────────────────────────────
3326
3327    #[test]
3328    fn test_validate_datatype_string() {
3329        assert!(rng_validate_datatype_value(Some("string"), "hello"));
3330        assert!(rng_validate_datatype_value(Some("string"), ""));
3331    }
3332
3333    #[test]
3334    fn test_validate_datatype_boolean() {
3335        assert!(rng_validate_datatype_value(Some("boolean"), "true"));
3336        assert!(rng_validate_datatype_value(Some("boolean"), "false"));
3337        assert!(rng_validate_datatype_value(Some("boolean"), "1"));
3338        assert!(rng_validate_datatype_value(Some("boolean"), "0"));
3339        assert!(!rng_validate_datatype_value(Some("boolean"), "yes"));
3340        assert!(!rng_validate_datatype_value(Some("boolean"), "no"));
3341    }
3342
3343    #[test]
3344    fn test_validate_datatype_integer() {
3345        assert!(rng_validate_datatype_value(Some("integer"), "42"));
3346        assert!(rng_validate_datatype_value(Some("integer"), "-42"));
3347        assert!(rng_validate_datatype_value(Some("integer"), "+42"));
3348        assert!(!rng_validate_datatype_value(Some("integer"), "12.5"));
3349        assert!(!rng_validate_datatype_value(Some("integer"), "abc"));
3350        assert!(!rng_validate_datatype_value(Some("integer"), ""));
3351    }
3352
3353    #[test]
3354    fn test_validate_datatype_decimal() {
3355        assert!(rng_validate_datatype_value(Some("decimal"), "42"));
3356        assert!(rng_validate_datatype_value(Some("decimal"), "12.5"));
3357        assert!(rng_validate_datatype_value(Some("decimal"), "-3.14"));
3358        assert!(!rng_validate_datatype_value(Some("decimal"), ""));
3359    }
3360
3361    #[test]
3362    fn test_validate_datatype_float() {
3363        assert!(rng_validate_datatype_value(Some("float"), "3.14"));
3364        assert!(rng_validate_datatype_value(Some("float"), "INF"));
3365        assert!(rng_validate_datatype_value(Some("float"), "-INF"));
3366        assert!(rng_validate_datatype_value(Some("float"), "NaN"));
3367        assert!(!rng_validate_datatype_value(Some("float"), ""));
3368    }
3369
3370    #[test]
3371    fn test_validate_datatype_ncname() {
3372        assert!(rng_validate_datatype_value(Some("NCName"), "myElement"));
3373        assert!(rng_validate_datatype_value(Some("NCName"), "_foo"));
3374        assert!(!rng_validate_datatype_value(Some("NCName"), "123abc"));
3375        assert!(!rng_validate_datatype_value(Some("NCName"), ""));
3376    }
3377
3378    #[test]
3379    fn test_validate_datatype_any_uri() {
3380        assert!(rng_validate_datatype_value(
3381            Some("anyURI"),
3382            "http://example.com"
3383        ));
3384        assert!(rng_validate_datatype_value(Some("anyURI"), "urn:isbn:1234"));
3385        assert!(!rng_validate_datatype_value(Some("anyURI"), ""));
3386        assert!(!rng_validate_datatype_value(Some("anyURI"), "has space"));
3387    }
3388
3389    #[test]
3390    fn test_validate_datatype_qname() {
3391        assert!(rng_validate_datatype_value(Some("QName"), "ns:local"));
3392        assert!(rng_validate_datatype_value(Some("QName"), "local"));
3393        assert!(!rng_validate_datatype_value(Some("QName"), ""));
3394    }
3395
3396    // ── C ABI Tests ───────────────────────────────────────────────────────
3397
3398    #[test]
3399    fn test_c_abi_new_parse_free() {
3400        let schema_xml = r#"<?xml version="1.0"?>
3401<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
3402  <text/>
3403</element>"#;
3404
3405        let ctxt = unsafe {
3406            xmlRelaxNGNewMemParserCtxt(
3407                schema_xml.as_ptr() as *const c_char,
3408                schema_xml.len() as c_int,
3409            )
3410        };
3411        assert!(!ctxt.is_null(), "Parser context should not be null");
3412
3413        let schema = unsafe { xmlRelaxNGParse(ctxt) };
3414        assert!(!schema.is_null(), "Schema should not be null");
3415
3416        // Free the schema
3417        unsafe { xmlRelaxNGFree(schema) };
3418    }
3419
3420    #[test]
3421    fn test_c_abi_validate_doc() {
3422        let schema_xml = r#"<?xml version="1.0"?>
3423<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
3424  <text/>
3425</element>"#;
3426
3427        let doc_xml = r#"<?xml version="1.0"?>
3428<root>Hello</root>"#;
3429
3430        let ctxt = unsafe {
3431            xmlRelaxNGNewMemParserCtxt(
3432                schema_xml.as_ptr() as *const c_char,
3433                schema_xml.len() as c_int,
3434            )
3435        };
3436        let schema = unsafe { xmlRelaxNGParse(ctxt) };
3437        assert!(!schema.is_null());
3438
3439        let valid_ctxt = unsafe { xmlRelaxNGNewValidCtxt(schema) };
3440        assert!(!valid_ctxt.is_null());
3441
3442        let doc = unsafe {
3443            crate::abi::exports_xml2::xmlReadMemory(
3444                doc_xml.as_ptr() as *const c_char,
3445                doc_xml.len() as c_int,
3446                c"test.xml".as_ptr() as *const c_char,
3447                ptr::null(),
3448                0,
3449            )
3450        };
3451        assert!(!doc.is_null());
3452
3453        let result = unsafe { xmlRelaxNGValidateDoc(valid_ctxt, doc) };
3454        assert_eq!(result, 0, "Validation should succeed");
3455
3456        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3457        unsafe { xmlRelaxNGFreeValidCtxt(valid_ctxt) };
3458        unsafe { xmlRelaxNGFree(schema) };
3459    }
3460
3461    #[test]
3462    fn test_c_abi_validate_full_element() {
3463        let schema_xml = r#"<?xml version="1.0"?>
3464<element name="item" xmlns="http://relaxng.org/ns/structure/1.0">
3465  <text/>
3466</element>"#;
3467
3468        let doc_xml = r#"<?xml version="1.0"?>
3469<root><item>Content</item></root>"#;
3470
3471        let ctxt = unsafe {
3472            xmlRelaxNGNewMemParserCtxt(
3473                schema_xml.as_ptr() as *const c_char,
3474                schema_xml.len() as c_int,
3475            )
3476        };
3477        let schema = unsafe { xmlRelaxNGParse(ctxt) };
3478        assert!(!schema.is_null());
3479
3480        let valid_ctxt = unsafe { xmlRelaxNGNewValidCtxt(schema) };
3481        assert!(!valid_ctxt.is_null());
3482
3483        let doc = unsafe {
3484            crate::abi::exports_xml2::xmlReadMemory(
3485                doc_xml.as_ptr() as *const c_char,
3486                doc_xml.len() as c_int,
3487                c"test.xml".as_ptr() as *const c_char,
3488                ptr::null(),
3489                0,
3490            )
3491        };
3492        assert!(!doc.is_null());
3493
3494        // Find the <item> element
3495        let item = unsafe {
3496            // Start from the first child of the document (root element)
3497            let mut node = (*doc).children;
3498            while !node.is_null() {
3499                if (*node).type_ == XML_ELEMENT_NODE as c_int {
3500                    break;
3501                }
3502                node = (*node).next;
3503            }
3504            if !node.is_null() {
3505                // Now find <item> child of root
3506                node = (*node).children;
3507                while !node.is_null() {
3508                    if (*node).type_ == XML_ELEMENT_NODE as c_int {
3509                        break;
3510                    }
3511                    node = (*node).next;
3512                }
3513            }
3514            node
3515        };
3516        assert!(!item.is_null(), "Should find <item> element");
3517
3518        let result = unsafe { xmlRelaxNGValidateFullElement(valid_ctxt, doc, item) };
3519        assert_eq!(result, 0, "Element validation should succeed");
3520
3521        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3522        unsafe { xmlRelaxNGFreeValidCtxt(valid_ctxt) };
3523        unsafe { xmlRelaxNGFree(schema) };
3524    }
3525
3526    #[test]
3527    fn test_c_abi_null_handling() {
3528        // Test null pointer handling
3529        assert_eq!(
3530            unsafe { xmlRelaxNGValidateDoc(ptr::null_mut(), ptr::null_mut()) },
3531            -1
3532        );
3533        assert_eq!(
3534            unsafe {
3535                xmlRelaxNGValidateFullElement(ptr::null_mut(), ptr::null_mut(), ptr::null_mut())
3536            },
3537            -1
3538        );
3539        assert!(unsafe { xmlRelaxNGNewMemParserCtxt(ptr::null(), 0).is_null() });
3540
3541        // Free with null should not crash
3542        unsafe { xmlRelaxNGFree(ptr::null_mut()) };
3543        unsafe { xmlRelaxNGFreeParserCtxt(ptr::null_mut()) };
3544        unsafe { xmlRelaxNGFreeValidCtxt(ptr::null_mut()) };
3545    }
3546
3547    // ── Edge Case Tests ───────────────────────────────────────────────────
3548
3549    #[test]
3550    fn test_parse_with_div() {
3551        let schema_xml = r#"<?xml version="1.0"?>
3552<grammar xmlns="http://relaxng.org/ns/structure/1.0">
3553  <div>
3554    <define name="shared">
3555      <text/>
3556    </define>
3557  </div>
3558  <start>
3559    <element name="root">
3560      <ref name="shared"/>
3561    </element>
3562  </start>
3563</grammar>"#;
3564
3565        let result = rng_parse(schema_xml);
3566        assert!(
3567            result.is_ok(),
3568            "Failed to parse with div: {:?}",
3569            result.err()
3570        );
3571        let schema = result.unwrap();
3572        assert_eq!(schema.grammar.defines.len(), 1);
3573        assert_eq!(schema.grammar.defines[0].name, "shared");
3574    }
3575
3576    #[test]
3577    fn test_validate_list_pattern() {
3578        let schema_xml = r#"<?xml version="1.0"?>
3579<element name="tokens" xmlns="http://relaxng.org/ns/structure/1.0">
3580  <list>
3581    <data type="token"/>
3582  </list>
3583</element>"#;
3584
3585        let doc_xml = r#"<?xml version="1.0"?>
3586<tokens>abc def ghi</tokens>"#;
3587
3588        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3589
3590        let doc = unsafe {
3591            crate::abi::exports_xml2::xmlReadMemory(
3592                doc_xml.as_ptr() as *const c_char,
3593                doc_xml.len() as c_int,
3594                c"test.xml".as_ptr() as *const c_char,
3595                ptr::null(),
3596                0,
3597            )
3598        };
3599        assert!(!doc.is_null());
3600
3601        let mut ctxt = RelaxNgValidCtxt::new();
3602        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3603        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3604
3605        assert!(valid, "List pattern validation failed: {:?}", ctxt.errors);
3606    }
3607
3608    #[test]
3609    fn test_validate_group_pattern() {
3610        let schema_xml = r#"<?xml version="1.0"?>
3611<grammar xmlns="http://relaxng.org/ns/structure/1.0">
3612  <start>
3613    <element name="root">
3614      <group>
3615        <element name="a">
3616          <text/>
3617        </element>
3618        <element name="b">
3619          <text/>
3620        </element>
3621      </group>
3622    </element>
3623  </start>
3624</grammar>"#;
3625
3626        let doc_xml = r#"<?xml version="1.0"?>
3627<root><a>First</a><b>Second</b></root>"#;
3628
3629        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3630
3631        let doc = unsafe {
3632            crate::abi::exports_xml2::xmlReadMemory(
3633                doc_xml.as_ptr() as *const c_char,
3634                doc_xml.len() as c_int,
3635                c"test.xml".as_ptr() as *const c_char,
3636                ptr::null(),
3637                0,
3638            )
3639        };
3640        assert!(!doc.is_null());
3641
3642        let mut ctxt = RelaxNgValidCtxt::new();
3643        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3644        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3645
3646        assert!(valid, "Group validation failed: {:?}", ctxt.errors);
3647    }
3648
3649    #[test]
3650    fn test_validate_undefined_ref() {
3651        let schema_xml = r#"<?xml version="1.0"?>
3652<grammar xmlns="http://relaxng.org/ns/structure/1.0">
3653  <start>
3654    <element name="root">
3655      <ref name="undefined"/>
3656    </element>
3657  </start>
3658</grammar>"#;
3659
3660        let doc_xml = r#"<?xml version="1.0"?>
3661<root>Content</root>"#;
3662
3663        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3664
3665        let doc = unsafe {
3666            crate::abi::exports_xml2::xmlReadMemory(
3667                doc_xml.as_ptr() as *const c_char,
3668                doc_xml.len() as c_int,
3669                c"test.xml".as_ptr() as *const c_char,
3670                ptr::null(),
3671                0,
3672            )
3673        };
3674        assert!(!doc.is_null());
3675
3676        let mut ctxt = RelaxNgValidCtxt::new();
3677        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3678        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3679
3680        assert!(!valid, "Undefined ref should cause failure");
3681    }
3682
3683    #[test]
3684    fn test_validate_null_doc() {
3685        let schema = RelaxNgSchema::new();
3686        let mut ctxt = RelaxNgValidCtxt::new();
3687        let valid = unsafe { rng_validate_doc(&schema, ptr::null_mut(), &mut ctxt) };
3688        assert!(!valid);
3689    }
3690
3691    #[test]
3692    fn test_parse_external_ref_schema() {
3693        let schema_xml = r#"<?xml version="1.0"?>
3694<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
3695  <externalRef href="external.rng"/>
3696</element>"#;
3697
3698        let result = rng_parse(schema_xml);
3699        assert!(
3700            result.is_ok(),
3701            "Failed to parse externalRef: {:?}",
3702            result.err()
3703        );
3704        let schema = result.unwrap();
3705        if let Some(ref start) = schema.grammar.start {
3706            assert_eq!(start.pattern_type, RelaxNgPatternType::Element);
3707            assert_eq!(start.name.as_deref(), Some("root"));
3708        }
3709    }
3710
3711    #[test]
3712    fn test_validate_boolean_datatype() {
3713        assert!(rng_validate_datatype_value(Some("boolean"), "true"));
3714        assert!(rng_validate_datatype_value(Some("boolean"), "false"));
3715        assert!(!rng_validate_datatype_value(Some("boolean"), "maybe"));
3716    }
3717
3718    #[test]
3719    fn test_validate_unknown_datatype() {
3720        // Unknown datatypes should be accepted (lenient behavior)
3721        assert!(rng_validate_datatype_value(Some("custom-type"), "anything"));
3722    }
3723
3724    #[test]
3725    fn test_validate_no_datatype() {
3726        // No datatype specified — accept anything
3727        assert!(rng_validate_datatype_value(None, "anything"));
3728    }
3729
3730    #[test]
3731    fn test_parse_schema_with_ns_prefix() {
3732        let schema_xml = r#"<?xml version="1.0"?>
3733<rng:element name="root" xmlns:rng="http://relaxng.org/ns/structure/1.0">
3734  <rng:text/>
3735</rng:element>"#;
3736
3737        let result = rng_parse(schema_xml);
3738        assert!(result.is_ok(), "Failed with ns prefix: {:?}", result.err());
3739    }
3740
3741    #[test]
3742    fn test_validation_context_path() {
3743        let mut ctxt = RelaxNgValidCtxt::new();
3744        assert_eq!(ctxt.current_path(), "/");
3745
3746        ctxt.path.push("root".to_string());
3747        assert_eq!(ctxt.current_path(), "/root");
3748
3749        ctxt.path.push("child".to_string());
3750        assert_eq!(ctxt.current_path(), "/root/child");
3751
3752        ctxt.path.pop();
3753        assert_eq!(ctxt.current_path(), "/root");
3754    }
3755
3756    #[test]
3757    fn test_validate_sequence_wrong_order() {
3758        let schema_xml = r#"<?xml version="1.0"?>
3759<grammar xmlns="http://relaxng.org/ns/structure/1.0">
3760  <start>
3761    <element name="root">
3762      <sequence>
3763        <element name="first">
3764          <text/>
3765        </element>
3766        <element name="second">
3767          <text/>
3768        </element>
3769      </sequence>
3770    </element>
3771  </start>
3772</grammar>"#;
3773
3774        let doc_xml = r#"<?xml version="1.0"?>
3775<root><second>Wrong</second><first>Order</first></root>"#;
3776
3777        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3778
3779        let doc = unsafe {
3780            crate::abi::exports_xml2::xmlReadMemory(
3781                doc_xml.as_ptr() as *const c_char,
3782                doc_xml.len() as c_int,
3783                c"test.xml".as_ptr() as *const c_char,
3784                ptr::null(),
3785                0,
3786            )
3787        };
3788        assert!(!doc.is_null());
3789
3790        let mut ctxt = RelaxNgValidCtxt::new();
3791        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3792        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3793
3794        // The sequence validates patterns against children in order,
3795        // so wrong order should fail
3796        assert!(!valid, "Wrong sequence order should fail");
3797    }
3798}