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/// # Safety
602///
603/// - `xml_doc` must be a valid `&str`; its bytes stay readable for the
604///   duration of the `xmlReadMemory` call.
605/// - The document pointer returned by `xmlReadMemory` is NULL-checked before
606///   being passed to `rng_parse_doc`, and is freed exactly once with
607///   `xmlFreeDoc`.
608///
609/// Returns the parsed schema, or an error message on failure.
610pub fn rng_parse(xml_doc: &str) -> Result<RelaxNgSchema, String> {
611    let doc_ptr = unsafe {
612        crate::abi::exports_xml2::xmlReadMemory(
613            xml_doc.as_ptr() as *const c_char,
614            xml_doc.len() as c_int,
615            c"schema.rng".as_ptr() as *const c_char,
616            ptr::null(),
617            0,
618        )
619    };
620
621    if doc_ptr.is_null() {
622        return Err("Failed to parse RELAX NG schema XML document".to_string());
623    }
624
625    let result = unsafe { rng_parse_doc(doc_ptr) };
626    unsafe {
627        crate::abi::exports_xml2::xmlFreeDoc(doc_ptr);
628    }
629    result
630}
631
632/// Parse a RELAX NG schema from a parsed XML document.
633///
634/// # SAFETY
635///
636/// - `doc` must be a valid pointer to an _xmlDoc representing a RELAX NG schema.
637unsafe fn rng_parse_doc(doc: *mut _xmlDoc) -> Result<RelaxNgSchema, String> {
638    unsafe {
639        let root = (*doc).children;
640        if root.is_null() {
641            return Err("RELAX NG document has no root element".to_string());
642        }
643
644        // Find the root element (skip any non-element nodes like comments)
645        let mut root_elem = root;
646        while !root_elem.is_null() && (*root_elem).type_ != XML_ELEMENT_NODE as c_int {
647            root_elem = (*root_elem).next;
648        }
649
650        if root_elem.is_null() {
651            return Err("RELAX NG document has no root element".to_string());
652        }
653
654        let local_name = get_local_name(root_elem);
655        let mut schema = RelaxNgSchema::new();
656
657        match local_name.as_str() {
658            "grammar" => {
659                // Top-level grammar
660                schema.grammar = rng_parse_grammar_node(root_elem, &mut schema);
661                Ok(schema)
662            }
663            "element" | "attribute" | "text" | "choice" | "sequence" | "interleave"
664            | "zeroOrMore" | "oneOrMore" | "optional" | "list" | "group" | "data" | "value"
665            | "ref" | "notAllowed" | "empty" | "externalRef" | "define" | "start" | "include" => {
666                // Single pattern as root (simplified grammar)
667                let pattern = rng_parse_pattern(root_elem, &mut schema);
668                schema.grammar.start = Some(pattern);
669                Ok(schema)
670            }
671            _ => Err(format!("Unknown RELAX NG root element: '{}'", local_name)),
672        }
673    }
674}
675
676/// Parse a `<grammar>` element.
677///
678/// # SAFETY
679///
680/// - `node` must be a valid pointer to a `<grammar>` element node.
681unsafe fn rng_parse_grammar_node(
682    node: *mut _xmlNode,
683    schema: &mut RelaxNgSchema,
684) -> RelaxNgGrammar {
685    unsafe {
686        let mut grammar = RelaxNgGrammar::new();
687
688        let mut child = (*node).children;
689        while !child.is_null() {
690            if (*child).type_ == XML_ELEMENT_NODE as c_int {
691                let local = get_local_name(child);
692                match local.as_str() {
693                    "define" => {
694                        let def = rng_parse_define(child, schema);
695                        grammar.defines.push(def);
696                    }
697                    "start" => {
698                        grammar.start = Some(rng_parse_pattern(child, schema));
699                    }
700                    "include" => {
701                        if let Some(inc) = rng_parse_include(child, schema) {
702                            grammar.includes.push(inc);
703                        }
704                    }
705                    "div" => {
706                        // <div> is a grouping element; recurse into it
707                        let sub_grammar = rng_parse_grammar_node(child, schema);
708                        grammar.defines.extend(sub_grammar.defines);
709                        if sub_grammar.start.is_some() {
710                            grammar.start = sub_grammar.start;
711                        }
712                        grammar.includes.extend(sub_grammar.includes);
713                    }
714                    _ => {
715                        // Unknown element inside grammar — treat as pattern error
716                        schema
717                            .errors
718                            .push(format!("Unexpected element '<{}>' in grammar", local));
719                    }
720                }
721            }
722            child = (*child).next;
723        }
724
725        grammar
726    }
727}
728
729/// Parse a `<define>` element.
730///
731/// # SAFETY
732///
733/// - `node` must be a valid pointer to a `<define>` element node.
734unsafe fn rng_parse_define(node: *mut _xmlNode, schema: &mut RelaxNgSchema) -> RelaxNgDefine {
735    unsafe {
736        let name = get_attr(node, "name").unwrap_or_default();
737        let pattern = rng_parse_pattern(node, schema);
738        RelaxNgDefine { name, pattern }
739    }
740}
741
742/// Parse an `<include>` element.
743///
744/// # SAFETY
745///
746/// - `node` must be a valid pointer to an `<include>` element node.
747unsafe fn rng_parse_include(
748    node: *mut _xmlNode,
749    _schema: &mut RelaxNgSchema,
750) -> Option<RelaxNgGrammar> {
751    unsafe {
752        let href = get_attr(node, "href");
753        if let Some(url) = href {
754            // Try to load and parse the external grammar
755            // For basic support, we attempt to read the file
756            let url_c = std::ffi::CString::new(url.clone()).ok()?;
757            let doc = crate::abi::exports_xml2::xmlParseFile(url_c.as_ptr());
758            if doc.is_null() {
759                return None;
760            }
761            let mut inc_schema = RelaxNgSchema::new();
762            let grammar = rng_parse_grammar_node(
763                {
764                    let mut root = (*doc).children;
765                    while !root.is_null() && (*root).type_ != XML_ELEMENT_NODE as c_int {
766                        root = (*root).next;
767                    }
768                    root
769                },
770                &mut inc_schema,
771            );
772            crate::abi::exports_xml2::xmlFreeDoc(doc);
773            Some(grammar)
774        } else {
775            // Inline grammar in include
776            let mut inc_schema = RelaxNgSchema::new();
777            let grammar = rng_parse_grammar_node(node, &mut inc_schema);
778            Some(grammar)
779        }
780    }
781}
782
783/// Parse a pattern from an element node. Dispatches based on element name.
784///
785/// # SAFETY
786///
787/// - `node` must be a valid pointer to an XML element node.
788unsafe fn rng_parse_pattern(node: *mut _xmlNode, schema: &mut RelaxNgSchema) -> RelaxNgPattern {
789    unsafe {
790        let local = get_local_name(node);
791
792        match local.as_str() {
793            "element" => rng_parse_element_pattern(node, schema),
794            "attribute" => rng_parse_attribute_pattern(node, schema),
795            "text" => RelaxNgPattern::text(),
796            "empty" => RelaxNgPattern::empty(),
797            "notAllowed" => RelaxNgPattern::not_allowed(),
798            "choice" => rng_parse_composite_pattern(node, RelaxNgPatternType::Choice, schema),
799            "sequence" => rng_parse_composite_pattern(node, RelaxNgPatternType::Sequence, schema),
800            "interleave" => {
801                rng_parse_composite_pattern(node, RelaxNgPatternType::Interleave, schema)
802            }
803            "zeroOrMore" => rng_parse_unary_pattern(node, RelaxNgPatternType::ZeroOrMore, schema),
804            "oneOrMore" => rng_parse_unary_pattern(node, RelaxNgPatternType::OneOrMore, schema),
805            "optional" => rng_parse_unary_pattern(node, RelaxNgPatternType::Optional, schema),
806            "list" => rng_parse_unary_pattern(node, RelaxNgPatternType::List, schema),
807            "group" => rng_parse_composite_pattern(node, RelaxNgPatternType::Group, schema),
808            "data" => rng_parse_data_pattern(node, schema),
809            "value" => rng_parse_value_pattern(node, schema),
810            "ref" => rng_parse_ref_pattern(node),
811            "externalRef" => rng_parse_external_ref(node, schema),
812            "define" | "start" | "grammar" | "include" | "div" => {
813                // These are grammar-level elements; return the content pattern
814                let mut child = (*node).children;
815                let mut result = RelaxNgPattern::empty();
816                while !child.is_null() {
817                    if (*child).type_ == XML_ELEMENT_NODE as c_int {
818                        result = rng_parse_pattern(child, schema);
819                        break;
820                    }
821                    child = (*child).next;
822                }
823                result
824            }
825            _ => {
826                // Unknown element — treat as empty pattern
827                schema
828                    .errors
829                    .push(format!("Unknown pattern element '<{}>'", local));
830                RelaxNgPattern::empty()
831            }
832        }
833    }
834}
835
836/// Parse an `<element>` pattern.
837///
838/// # SAFETY
839///
840/// - `node` must be a valid pointer to an `<element>` element node.
841unsafe fn rng_parse_element_pattern(
842    node: *mut _xmlNode,
843    schema: &mut RelaxNgSchema,
844) -> RelaxNgPattern {
845    unsafe {
846        let mut pattern = RelaxNgPattern::new(RelaxNgPatternType::Element);
847
848        // Parse name class from name attribute or child <name>, <anyName>, <nsName>, <choice>
849        let name_attr = get_attr(node, "name");
850        pattern.name = name_attr.clone();
851
852        if let Some(ref n) = name_attr {
853            pattern.name_class = Some(RelaxNgNameClass::Name(n.clone()));
854        }
855
856        // Parse children for name class and content pattern
857        let mut child = (*node).children;
858        let mut content_found = false;
859
860        while !child.is_null() {
861            if (*child).type_ == XML_ELEMENT_NODE as c_int {
862                let child_local = get_local_name(child);
863
864                match child_local.as_str() {
865                    "name" => {
866                        let text = get_node_text(child);
867                        if !text.is_empty() {
868                            pattern.name_class =
869                                Some(RelaxNgNameClass::Name(text.trim().to_string()));
870                        }
871                    }
872                    "anyName" => {
873                        pattern.name_class = Some(rng_parse_any_name(child));
874                    }
875                    "nsName" => {
876                        pattern.name_class = Some(rng_parse_ns_name(child));
877                    }
878                    "choice" if pattern.name_class.is_none() => {
879                        // Name class choice (only before content)
880                        pattern.name_class = Some(rng_parse_name_class_choice(child));
881                    }
882                    _ => {
883                        // Content pattern
884                        if !content_found {
885                            pattern.children.push(rng_parse_pattern(child, schema));
886                            content_found = true;
887                        }
888                    }
889                }
890            }
891            child = (*child).next;
892        }
893
894        pattern
895    }
896}
897
898/// Parse an `<attribute>` pattern.
899///
900/// # SAFETY
901///
902/// - `node` must be a valid pointer to an `<attribute>` element node.
903unsafe fn rng_parse_attribute_pattern(
904    node: *mut _xmlNode,
905    schema: &mut RelaxNgSchema,
906) -> RelaxNgPattern {
907    unsafe {
908        let mut pattern = RelaxNgPattern::new(RelaxNgPatternType::Attribute);
909
910        // Parse name class
911        let name_attr = get_attr(node, "name");
912        pattern.name = name_attr.clone();
913
914        if let Some(ref n) = name_attr {
915            pattern.name_class = Some(RelaxNgNameClass::Name(n.clone()));
916        }
917
918        // Parse children
919        let mut child = (*node).children;
920        while !child.is_null() {
921            if (*child).type_ == XML_ELEMENT_NODE as c_int {
922                let child_local = get_local_name(child);
923
924                match child_local.as_str() {
925                    "name" => {
926                        let text = get_node_text(child);
927                        if !text.is_empty() {
928                            pattern.name_class =
929                                Some(RelaxNgNameClass::Name(text.trim().to_string()));
930                        }
931                    }
932                    "anyName" => {
933                        pattern.name_class = Some(rng_parse_any_name(child));
934                    }
935                    "nsName" => {
936                        pattern.name_class = Some(rng_parse_ns_name(child));
937                    }
938                    "choice" if pattern.name_class.is_none() => {
939                        pattern.name_class = Some(rng_parse_name_class_choice(child));
940                    }
941                    _ => {
942                        // Content pattern (text, data, etc.)
943                        pattern.children.push(rng_parse_pattern(child, schema));
944                    }
945                }
946            }
947            child = (*child).next;
948        }
949
950        pattern
951    }
952}
953
954/// Parse an `<anyName>` element (possibly with `<except>`).
955///
956/// # SAFETY
957///
958/// - `node` must be a valid pointer to an `<anyName>` element node.
959unsafe fn rng_parse_any_name(node: *mut _xmlNode) -> RelaxNgNameClass {
960    unsafe {
961        // Check for <except> child
962        let mut child = (*node).children;
963        while !child.is_null() {
964            if (*child).type_ == XML_ELEMENT_NODE as c_int && get_local_name(child) == "except" {
965                let except_nc = rng_parse_name_class_content(child);
966                return RelaxNgNameClass::Except(
967                    Box::new(RelaxNgNameClass::AnyName),
968                    Box::new(except_nc),
969                );
970            }
971            child = (*child).next;
972        }
973        RelaxNgNameClass::AnyName
974    }
975}
976
977/// Parse an `<nsName>` element (possibly with `<except>`).
978///
979/// # SAFETY
980///
981/// - `node` must be a valid pointer to an `<nsName>` element node.
982unsafe fn rng_parse_ns_name(node: *mut _xmlNode) -> RelaxNgNameClass {
983    unsafe {
984        let ns = get_attr(node, "ns").unwrap_or_default();
985
986        // Check for <except> child
987        let mut child = (*node).children;
988        while !child.is_null() {
989            if (*child).type_ == XML_ELEMENT_NODE as c_int && get_local_name(child) == "except" {
990                let except_nc = rng_parse_name_class_content(child);
991                return RelaxNgNameClass::Except(
992                    Box::new(RelaxNgNameClass::NsName(ns)),
993                    Box::new(except_nc),
994                );
995            }
996            child = (*child).next;
997        }
998
999        RelaxNgNameClass::NsName(ns)
1000    }
1001}
1002
1003/// Parse name class children of a `<choice>` element used as name class.
1004///
1005/// # SAFETY
1006///
1007/// - `node` must be a valid pointer to an element node containing name class children.
1008unsafe fn rng_parse_name_class_choice(node: *mut _xmlNode) -> RelaxNgNameClass {
1009    unsafe {
1010        let mut choices = Vec::new();
1011        let mut child = (*node).children;
1012        while !child.is_null() {
1013            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1014                choices.push(rng_parse_name_class_item(child));
1015            }
1016            child = (*child).next;
1017        }
1018        if choices.len() == 1 {
1019            choices.remove(0)
1020        } else {
1021            RelaxNgNameClass::Choice(choices)
1022        }
1023    }
1024}
1025
1026/// Parse a single name class item from a name class context.
1027///
1028/// # SAFETY
1029///
1030/// - `node` must be a valid pointer to an element node.
1031unsafe fn rng_parse_name_class_item(node: *mut _xmlNode) -> RelaxNgNameClass {
1032    unsafe {
1033        let local = get_local_name(node);
1034        match local.as_str() {
1035            "name" => {
1036                let text = get_node_text(node);
1037                RelaxNgNameClass::Name(text.trim().to_string())
1038            }
1039            "anyName" => rng_parse_any_name(node),
1040            "nsName" => rng_parse_ns_name(node),
1041            "choice" => rng_parse_name_class_choice(node),
1042            _ => {
1043                // Default: treat as name
1044                let text = get_node_text(node);
1045                if text.trim().is_empty() {
1046                    RelaxNgNameClass::AnyName
1047                } else {
1048                    RelaxNgNameClass::Name(text.trim().to_string())
1049                }
1050            }
1051        }
1052    }
1053}
1054
1055/// Parse name class content from an `<except>` or similar element.
1056///
1057/// # SAFETY
1058///
1059/// - `node` must be a valid pointer to an element node.
1060unsafe fn rng_parse_name_class_content(node: *mut _xmlNode) -> RelaxNgNameClass {
1061    unsafe {
1062        let mut names = Vec::new();
1063        let mut child = (*node).children;
1064        while !child.is_null() {
1065            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1066                names.push(rng_parse_name_class_item(child));
1067            }
1068            child = (*child).next;
1069        }
1070        if names.is_empty() {
1071            RelaxNgNameClass::AnyName
1072        } else if names.len() == 1 {
1073            names.remove(0)
1074        } else {
1075            RelaxNgNameClass::Choice(names)
1076        }
1077    }
1078}
1079
1080/// Parse a composite pattern (sequence, choice, interleave, group).
1081///
1082/// # SAFETY
1083///
1084/// - `node` must be a valid pointer to an element node.
1085unsafe fn rng_parse_composite_pattern(
1086    node: *mut _xmlNode,
1087    pattern_type: RelaxNgPatternType,
1088    schema: &mut RelaxNgSchema,
1089) -> RelaxNgPattern {
1090    unsafe {
1091        let mut pattern = RelaxNgPattern::new(pattern_type);
1092
1093        let mut child = (*node).children;
1094        while !child.is_null() {
1095            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1096                pattern.children.push(rng_parse_pattern(child, schema));
1097            }
1098            child = (*child).next;
1099        }
1100
1101        pattern
1102    }
1103}
1104
1105/// Parse a unary pattern (zeroOrMore, oneOrMore, optional, list).
1106///
1107/// # SAFETY
1108///
1109/// - `node` must be a valid pointer to an element node.
1110unsafe fn rng_parse_unary_pattern(
1111    node: *mut _xmlNode,
1112    pattern_type: RelaxNgPatternType,
1113    schema: &mut RelaxNgSchema,
1114) -> RelaxNgPattern {
1115    unsafe {
1116        let mut pattern = RelaxNgPattern::new(pattern_type);
1117
1118        let mut child = (*node).children;
1119        while !child.is_null() {
1120            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1121                pattern.children.push(rng_parse_pattern(child, schema));
1122                // Only take the first child pattern for unary patterns
1123                break;
1124            }
1125            child = (*child).next;
1126        }
1127
1128        pattern
1129    }
1130}
1131
1132/// Parse a `<data>` pattern.
1133///
1134/// # SAFETY
1135///
1136/// - `node` must be a valid pointer to a `<data>` element node.
1137unsafe fn rng_parse_data_pattern(
1138    node: *mut _xmlNode,
1139    _schema: &mut RelaxNgSchema,
1140) -> RelaxNgPattern {
1141    unsafe {
1142        let mut pattern = RelaxNgPattern::new(RelaxNgPatternType::Data);
1143        pattern.datatype = get_attr(node, "type");
1144        pattern.datatype_library = get_attr(node, "datatypeLibrary");
1145
1146        // For basic support, we store the type and don't process params in detail
1147        pattern
1148    }
1149}
1150
1151/// Parse a `<value>` pattern.
1152///
1153/// # SAFETY
1154///
1155/// - `node` must be a valid pointer to a `<value>` element node.
1156unsafe fn rng_parse_value_pattern(
1157    node: *mut _xmlNode,
1158    _schema: &mut RelaxNgSchema,
1159) -> RelaxNgPattern {
1160    unsafe {
1161        let mut pattern = RelaxNgPattern::new(RelaxNgPatternType::Value);
1162        pattern.datatype = get_attr(node, "type");
1163        pattern.datatype_library = get_attr(node, "datatypeLibrary");
1164        pattern.value = Some(get_node_text(node).trim().to_string());
1165
1166        pattern
1167    }
1168}
1169
1170/// Parse a `<ref>` pattern.
1171///
1172/// # SAFETY
1173///
1174/// - `node` must be a valid pointer to a `<ref>` element node.
1175unsafe fn rng_parse_ref_pattern(node: *mut _xmlNode) -> RelaxNgPattern {
1176    unsafe {
1177        let mut pattern = RelaxNgPattern::new(RelaxNgPatternType::Ref);
1178        pattern.name = get_attr(node, "name");
1179        pattern
1180    }
1181}
1182
1183/// Parse an `<externalRef>` pattern.
1184///
1185/// # SAFETY
1186///
1187/// - `node` must be a valid pointer to an `<externalRef>` element node.
1188unsafe fn rng_parse_external_ref(
1189    node: *mut _xmlNode,
1190    _schema: &mut RelaxNgSchema,
1191) -> RelaxNgPattern {
1192    unsafe {
1193        let mut pattern = RelaxNgPattern::new(RelaxNgPatternType::ExternalRef);
1194        let href = get_attr(node, "href");
1195        pattern.name = href;
1196        pattern
1197    }
1198}
1199
1200// ═══════════════════════════════════════════════════════════════════════════════
1201// RELAX NG Validation Logic
1202// ═══════════════════════════════════════════════════════════════════════════════
1203
1204/// Validate an XML document against a RELAX NG schema.
1205///
1206/// Returns `true` if the document is valid.
1207///
1208/// # SAFETY
1209///
1210/// - `schema` must be a valid reference to a parsed schema.
1211/// - `doc` must be a valid pointer to an _xmlDoc or NULL.
1212/// - `ctxt` must be a valid mutable reference to a validation context.
1213pub unsafe fn rng_validate_doc(
1214    schema: &RelaxNgSchema,
1215    doc: *mut _xmlDoc,
1216    ctxt: &mut RelaxNgValidCtxt,
1217) -> bool {
1218    unsafe {
1219        if doc.is_null() {
1220            ctxt.record_error("Document is null".to_string());
1221            return false;
1222        }
1223
1224        let root = (*doc).children;
1225        if root.is_null() {
1226            ctxt.record_error("Document has no children".to_string());
1227            return false;
1228        }
1229
1230        // Find the root element
1231        let mut root_elem = root;
1232        while !root_elem.is_null() && (*root_elem).type_ != XML_ELEMENT_NODE as c_int {
1233            root_elem = (*root_elem).next;
1234        }
1235
1236        if root_elem.is_null() {
1237            ctxt.record_error("Document has no root element".to_string());
1238            return false;
1239        }
1240
1241        // Get the start pattern
1242        let start_pattern = match &schema.grammar.start {
1243            Some(p) => p,
1244            None => {
1245                ctxt.record_error("Schema has no start pattern".to_string());
1246                return false;
1247            }
1248        };
1249
1250        ctxt.path.clear();
1251        let valid = rng_validate_pattern(start_pattern, root_elem, schema, ctxt);
1252
1253        // Check for remaining unmatched errors
1254        valid
1255    }
1256}
1257
1258/// Validate a pattern against a node.
1259///
1260/// # SAFETY
1261///
1262/// - `node` must be a valid pointer to an _xmlNode or NULL.
1263fn rng_validate_pattern(
1264    pattern: &RelaxNgPattern,
1265    node: *mut _xmlNode,
1266    schema: &RelaxNgSchema,
1267    ctxt: &mut RelaxNgValidCtxt,
1268) -> bool {
1269    unsafe {
1270        if ctxt.depth >= ctxt.depth_max {
1271            ctxt.record_error("Maximum validation depth exceeded".to_string());
1272            return false;
1273        }
1274        ctxt.depth += 1;
1275
1276        let result = match pattern.pattern_type {
1277            RelaxNgPatternType::Element => {
1278                rng_validate_element_pattern(pattern, node, schema, ctxt)
1279            }
1280            RelaxNgPatternType::Attribute => {
1281                rng_validate_attribute_pattern(pattern, node, schema, ctxt)
1282            }
1283            RelaxNgPatternType::Text => rng_validate_text_pattern(node, ctxt),
1284            RelaxNgPatternType::Empty => rng_validate_empty_pattern(node, ctxt),
1285            RelaxNgPatternType::NotAllowed => {
1286                let name = get_node_qname(node);
1287                ctxt.record_error(format!(
1288                    "Element '{}' is not allowed at '{}'",
1289                    name,
1290                    ctxt.current_path()
1291                ));
1292                false
1293            }
1294            RelaxNgPatternType::Choice => rng_validate_choice_pattern(pattern, node, schema, ctxt),
1295            RelaxNgPatternType::Sequence => {
1296                rng_validate_sequence_pattern(pattern, node, schema, ctxt)
1297            }
1298            RelaxNgPatternType::Interleave => {
1299                rng_validate_interleave_pattern(pattern, node, schema, ctxt)
1300            }
1301            RelaxNgPatternType::ZeroOrMore => {
1302                rng_validate_zero_or_more(pattern, node, schema, ctxt)
1303            }
1304            RelaxNgPatternType::OneOrMore => rng_validate_one_or_more(pattern, node, schema, ctxt),
1305            RelaxNgPatternType::Optional => {
1306                rng_validate_optional_pattern(pattern, node, schema, ctxt)
1307            }
1308            RelaxNgPatternType::List => rng_validate_list_pattern(pattern, node, schema, ctxt),
1309            RelaxNgPatternType::Group => rng_validate_group_pattern(pattern, node, schema, ctxt),
1310            RelaxNgPatternType::Data => rng_validate_data_pattern(pattern, node, ctxt),
1311            RelaxNgPatternType::Value => rng_validate_value_pattern(pattern, node, ctxt),
1312            RelaxNgPatternType::Ref => rng_validate_ref_pattern(pattern, node, schema, ctxt),
1313            RelaxNgPatternType::ExternalRef => {
1314                // External refs are resolved during parsing; treat as empty
1315                rng_validate_empty_pattern(node, ctxt)
1316            }
1317            RelaxNgPatternType::Define
1318            | RelaxNgPatternType::Grammar
1319            | RelaxNgPatternType::Start
1320            | RelaxNgPatternType::Include => {
1321                // These shouldn't appear during validation; treat as pass-through
1322                rng_validate_children(pattern, node, schema, ctxt)
1323            }
1324        };
1325
1326        ctxt.depth -= 1;
1327        result
1328    }
1329}
1330
1331/// Validate a pattern's children against a node's children.
1332///
1333/// # SAFETY
1334///
1335/// - `node` must be a valid pointer to an _xmlNode or NULL.
1336fn rng_validate_children(
1337    pattern: &RelaxNgPattern,
1338    node: *mut _xmlNode,
1339    schema: &RelaxNgSchema,
1340    ctxt: &mut RelaxNgValidCtxt,
1341) -> bool {
1342    {
1343        if pattern.children.is_empty() {
1344            return true;
1345        }
1346        // Validate each child pattern against the same node
1347        let mut valid = true;
1348        for child in &pattern.children {
1349            valid &= rng_validate_pattern(child, node, schema, ctxt);
1350        }
1351        valid
1352    }
1353}
1354
1355/// Validate an element pattern against an element node.
1356///
1357/// # SAFETY
1358///
1359/// - `node` must be a valid pointer to an _xmlNode or NULL.
1360fn rng_validate_element_pattern(
1361    pattern: &RelaxNgPattern,
1362    node: *mut _xmlNode,
1363    schema: &RelaxNgSchema,
1364    ctxt: &mut RelaxNgValidCtxt,
1365) -> bool {
1366    unsafe {
1367        if node.is_null() || (*node).type_ != XML_ELEMENT_NODE as c_int {
1368            return false;
1369        }
1370
1371        let node_name = get_node_qname(node);
1372        let ns_uri = get_node_ns_uri(node);
1373
1374        // Check if the node name matches the element pattern's name class
1375        if let Some(ref nc) = pattern.name_class {
1376            if !nc.matches(&node_name, ns_uri.as_deref()) {
1377                let pat_name = pattern.name.as_deref().unwrap_or("?");
1378                ctxt.record_error(format!(
1379                    "Element '{}' does not match expected element pattern '{}' at '{}'",
1380                    node_name,
1381                    pat_name,
1382                    ctxt.current_path()
1383                ));
1384                return false;
1385            }
1386        }
1387
1388        // Push the element name onto the path
1389        ctxt.path.push(node_name.clone());
1390
1391        // Validate child patterns against this element
1392        let mut valid = true;
1393        if pattern.children.is_empty() {
1394            // No content pattern — element must be empty
1395            let mut child = (*node).children;
1396            while !child.is_null() {
1397                if (*child).type_ == XML_ELEMENT_NODE as c_int {
1398                    let child_name = get_node_qname(child);
1399                    ctxt.record_error(format!(
1400                        "Unexpected child element '{}' in empty element '{}' at '{}'",
1401                        child_name,
1402                        node_name,
1403                        ctxt.current_path()
1404                    ));
1405                    valid = false;
1406                }
1407                child = (*child).next;
1408            }
1409        } else {
1410            // Validate the content pattern against this element
1411            // For element patterns, the child pattern validates the element's content
1412            for child_pat in &pattern.children {
1413                valid &= rng_validate_pattern(child_pat, node, schema, ctxt);
1414            }
1415        }
1416
1417        ctxt.path.pop();
1418        valid
1419    }
1420}
1421
1422/// Validate an attribute pattern.
1423///
1424/// # SAFETY
1425///
1426/// - `node` must be a valid pointer to an _xmlNode or NULL.
1427fn rng_validate_attribute_pattern(
1428    pattern: &RelaxNgPattern,
1429    node: *mut _xmlNode,
1430    _schema: &RelaxNgSchema,
1431    ctxt: &mut RelaxNgValidCtxt,
1432) -> bool {
1433    unsafe {
1434        if node.is_null() || (*node).type_ != XML_ELEMENT_NODE as c_int {
1435            return false;
1436        }
1437
1438        // Get the attribute name from the pattern's name class
1439        let attr_name = match &pattern.name_class {
1440            Some(RelaxNgNameClass::Name(n)) => n.clone(),
1441            Some(RelaxNgNameClass::AnyName) => {
1442                // Any attribute is allowed — just check that there's at least one
1443                // attribute that matches (or return true if the attribute is optional)
1444                // For now, we just return true for anyName since we can't know which
1445                // attribute to check
1446                return true;
1447            }
1448            Some(RelaxNgNameClass::NsName(ns)) => {
1449                // Any attribute in the given namespace.
1450                // Check if there's an attribute with a matching namespace.
1451                let mut prop = (*node).properties;
1452                while !prop.is_null() {
1453                    let prop_ns = get_node_ns_uri(prop as *mut _xmlNode);
1454                    if let Some(ref uri) = prop_ns {
1455                        if uri == ns {
1456                            // Validate content pattern against attribute value
1457                            if let Some(content) = &pattern.children.first() {
1458                                let val = get_node_text(prop as *mut _xmlNode);
1459                                let valid = match content.pattern_type {
1460                                    RelaxNgPatternType::Text => true,
1461                                    RelaxNgPatternType::Data => rng_validate_datatype_value(
1462                                        content.datatype.as_deref(),
1463                                        &val,
1464                                    ),
1465                                    RelaxNgPatternType::Value => {
1466                                        content.value.as_deref() == Some(&val)
1467                                    }
1468                                    _ => true,
1469                                };
1470                                if !valid {
1471                                    ctxt.record_error(format!(
1472                                        "Attribute '{}' has invalid value at '{}'",
1473                                        prop_ns.unwrap_or_default(),
1474                                        ctxt.current_path()
1475                                    ));
1476                                    return false;
1477                                }
1478                            }
1479                            return true;
1480                        }
1481                    }
1482                    prop = (*prop).next;
1483                }
1484                // No matching attribute found — attribute is required
1485                // (In RELAX NG, attributes are implicitly required)
1486                ctxt.record_error(format!(
1487                    "Required attribute in namespace '{}' is missing at '{}'",
1488                    ns,
1489                    ctxt.current_path()
1490                ));
1491                return false;
1492            }
1493            _ => {
1494                // Complex name class — just check if any attribute matches
1495                // This is a simplified check
1496                return true;
1497            }
1498        };
1499
1500        // Check if the attribute exists on the element
1501        let attr_value = get_attr(node, &attr_name);
1502
1503        match attr_value {
1504            Some(val) => {
1505                // Validate content pattern against attribute value
1506                if let Some(content) = pattern.children.first() {
1507                    let valid = match content.pattern_type {
1508                        RelaxNgPatternType::Text => true,
1509                        RelaxNgPatternType::Data => {
1510                            rng_validate_datatype_value(content.datatype.as_deref(), &val)
1511                        }
1512                        RelaxNgPatternType::Value => content.value.as_deref() == Some(&val),
1513                        _ => true,
1514                    };
1515                    if !valid {
1516                        ctxt.record_error(format!(
1517                            "Attribute '{}' has invalid value '{}' at '{}'",
1518                            attr_name,
1519                            val,
1520                            ctxt.current_path()
1521                        ));
1522                        return false;
1523                    }
1524                }
1525                true
1526            }
1527            None => {
1528                // Attribute not found — only error if the pattern requires it
1529                // (In RELAX NG, attributes are implicitly required unless wrapped in optional)
1530                ctxt.record_error(format!(
1531                    "Required attribute '{}' is missing at '{}'",
1532                    attr_name,
1533                    ctxt.current_path()
1534                ));
1535                false
1536            }
1537        }
1538    }
1539}
1540
1541/// Validate a text pattern against text content.
1542///
1543/// # SAFETY
1544///
1545/// - `node` must be a valid pointer to an _xmlNode or NULL.
1546const fn rng_validate_text_pattern(node: *mut _xmlNode, _ctxt: &mut RelaxNgValidCtxt) -> bool {
1547    {
1548        if node.is_null() {
1549            return false;
1550        }
1551        // Text pattern matches any text content (or mixed content with elements)
1552        // In RELAX NG, text allows any text content
1553        true
1554    }
1555}
1556
1557/// Validate an empty pattern.
1558///
1559/// # SAFETY
1560///
1561/// - `node` must be a valid pointer to an _xmlNode or NULL.
1562fn rng_validate_empty_pattern(node: *mut _xmlNode, ctxt: &mut RelaxNgValidCtxt) -> bool {
1563    unsafe {
1564        if node.is_null() {
1565            return true;
1566        }
1567        // Empty pattern — the element must have no element children
1568        let mut child = (*node).children;
1569        while !child.is_null() {
1570            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1571                let child_name = get_node_qname(child);
1572                ctxt.record_error(format!(
1573                    "Unexpected child element '{}' in empty content at '{}'",
1574                    child_name,
1575                    ctxt.current_path()
1576                ));
1577                return false;
1578            }
1579            child = (*child).next;
1580        }
1581        true
1582    }
1583}
1584
1585/// Validate a choice pattern.
1586///
1587/// # SAFETY
1588///
1589/// - `node` must be a valid pointer to an _xmlNode or NULL.
1590fn rng_validate_choice_pattern(
1591    pattern: &RelaxNgPattern,
1592    node: *mut _xmlNode,
1593    schema: &RelaxNgSchema,
1594    ctxt: &mut RelaxNgValidCtxt,
1595) -> bool {
1596    unsafe {
1597        if pattern.children.is_empty() {
1598            return false;
1599        }
1600
1601        // At least one choice must match
1602        let mut last_error = String::new();
1603        for child in &pattern.children {
1604            let saved_errors = ctxt.errors.len();
1605            let saved_nb = ctxt.nb_errors;
1606
1607            if rng_validate_pattern(child, node, schema, ctxt) {
1608                // Restore errors from failed choices
1609                // (errors from failed branches should be discarded)
1610                return true;
1611            }
1612
1613            // Capture the last error for reporting
1614            if ctxt.errors.len() > saved_errors {
1615                last_error = ctxt.errors.last().unwrap().clone();
1616            }
1617
1618            // Restore error state (choice means at least one alternative must pass)
1619            ctxt.errors.truncate(saved_errors);
1620            ctxt.nb_errors = saved_nb;
1621        }
1622
1623        // If we got here, none of the choices matched
1624        let node_name = if node.is_null() {
1625            "null".to_string()
1626        } else {
1627            get_node_qname(node)
1628        };
1629        ctxt.record_error(format!(
1630            "No choice pattern matched for '{}' at '{}'. Last error: {}",
1631            node_name,
1632            ctxt.current_path(),
1633            last_error
1634        ));
1635        false
1636    }
1637}
1638
1639/// Validate a sequence pattern.
1640///
1641/// # SAFETY
1642///
1643/// - `node` must be a valid pointer to an _xmlNode or NULL.
1644fn rng_validate_sequence_pattern(
1645    pattern: &RelaxNgPattern,
1646    node: *mut _xmlNode,
1647    schema: &RelaxNgSchema,
1648    ctxt: &mut RelaxNgValidCtxt,
1649) -> bool {
1650    unsafe {
1651        if node.is_null() {
1652            return pattern.children.is_empty();
1653        }
1654
1655        let mut valid = true;
1656
1657        // For sequence validation, we validate each child pattern against
1658        // the node's children in order.
1659        // Collect element children of the node
1660        let mut child_nodes: Vec<*mut _xmlNode> = Vec::new();
1661        let mut child = (*node).children;
1662        while !child.is_null() {
1663            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1664                child_nodes.push(child);
1665            }
1666            child = (*child).next;
1667        }
1668
1669        // Simple sequence matching: validate each child pattern
1670        // against corresponding child nodes
1671        let mut child_idx = 0;
1672        for child_pat in &pattern.children {
1673            if child_idx >= child_nodes.len() {
1674                // Check if the pattern is optional or zero-or-more
1675                match child_pat.pattern_type {
1676                    RelaxNgPatternType::Optional | RelaxNgPatternType::ZeroOrMore => {
1677                        // These are fine to have no matching children
1678                        continue;
1679                    }
1680                    RelaxNgPatternType::OneOrMore => {
1681                        ctxt.record_error(format!(
1682                            "Expected at least one matching child for oneOrMore at '{}'",
1683                            ctxt.current_path()
1684                        ));
1685                        valid = false;
1686                        continue;
1687                    }
1688                    _ => {
1689                        ctxt.record_error(format!(
1690                            "Expected more child elements for sequence at '{}'",
1691                            ctxt.current_path()
1692                        ));
1693                        valid = false;
1694                        continue;
1695                    }
1696                }
1697            }
1698
1699            let child_node = child_nodes[child_idx];
1700            valid &= rng_validate_pattern(child_pat, child_node, schema, ctxt);
1701            child_idx += 1;
1702        }
1703
1704        // Check for extra children
1705        if child_idx < child_nodes.len() {
1706            let extra_name = get_node_qname(child_nodes[child_idx]);
1707            ctxt.record_error(format!(
1708                "Unexpected extra element '{}' in sequence at '{}'",
1709                extra_name,
1710                ctxt.current_path()
1711            ));
1712            valid = false;
1713        }
1714
1715        valid
1716    }
1717}
1718
1719/// Validate an interleave pattern.
1720///
1721/// # SAFETY
1722///
1723/// - `node` must be a valid pointer to an _xmlNode or NULL.
1724fn rng_validate_interleave_pattern(
1725    pattern: &RelaxNgPattern,
1726    node: *mut _xmlNode,
1727    schema: &RelaxNgSchema,
1728    ctxt: &mut RelaxNgValidCtxt,
1729) -> bool {
1730    unsafe {
1731        if node.is_null() {
1732            return pattern.children.is_empty();
1733        }
1734
1735        // Interleave means child patterns can match in any order.
1736        // For simplicity, we validate each child pattern against a fresh
1737        // traversal of the node's children.
1738        let mut valid = true;
1739
1740        for child_pat in &pattern.children {
1741            // For each pattern in the interleave, check if there's at least
1742            // one child node that matches
1743            let mut child = (*node).children;
1744            let mut matched = false;
1745
1746            while !child.is_null() {
1747                if (*child).type_ == XML_ELEMENT_NODE as c_int {
1748                    let saved_errors = ctxt.errors.len();
1749                    let saved_nb = ctxt.nb_errors;
1750
1751                    if rng_validate_pattern(child_pat, child, schema, ctxt) {
1752                        matched = true;
1753                        break;
1754                    }
1755
1756                    // Restore errors for this attempt
1757                    ctxt.errors.truncate(saved_errors);
1758                    ctxt.nb_errors = saved_nb;
1759                }
1760                child = (*child).next;
1761            }
1762
1763            if !matched {
1764                // Check if pattern is optional
1765                match child_pat.pattern_type {
1766                    RelaxNgPatternType::Optional | RelaxNgPatternType::ZeroOrMore => {
1767                        // Optional patterns can be absent
1768                    }
1769                    _ => {
1770                        let pat_desc = format!("{:?}", child_pat.pattern_type);
1771                        ctxt.record_error(format!(
1772                            "Interleave pattern '{}' did not match any child at '{}'",
1773                            pat_desc,
1774                            ctxt.current_path()
1775                        ));
1776                        valid = false;
1777                    }
1778                }
1779            }
1780        }
1781
1782        valid
1783    }
1784}
1785
1786/// Validate a zeroOrMore pattern.
1787///
1788/// # SAFETY
1789///
1790/// - `node` must be a valid pointer to an _xmlNode or NULL.
1791fn rng_validate_zero_or_more(
1792    pattern: &RelaxNgPattern,
1793    node: *mut _xmlNode,
1794    schema: &RelaxNgSchema,
1795    ctxt: &mut RelaxNgValidCtxt,
1796) -> bool {
1797    unsafe {
1798        if node.is_null() || pattern.children.is_empty() {
1799            return true;
1800        }
1801
1802        let child_pat = &pattern.children[0];
1803        let valid = true;
1804
1805        // Match as many child nodes as possible against the pattern
1806        let mut child = (*node).children;
1807        while !child.is_null() {
1808            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1809                let saved_errors = ctxt.errors.len();
1810                let saved_nb = ctxt.nb_errors;
1811
1812                if !rng_validate_pattern(child_pat, child, schema, ctxt) {
1813                    // This child doesn't match the zeroOrMore pattern
1814                    // Restore errors and stop
1815                    ctxt.errors.truncate(saved_errors);
1816                    ctxt.nb_errors = saved_nb;
1817                    break;
1818                }
1819                // Successfully matched — errors from matching are valid
1820            }
1821            child = (*child).next;
1822        }
1823
1824        valid
1825    }
1826}
1827
1828/// Validate a oneOrMore pattern.
1829///
1830/// # SAFETY
1831///
1832/// - `node` must be a valid pointer to an _xmlNode or NULL.
1833fn rng_validate_one_or_more(
1834    pattern: &RelaxNgPattern,
1835    node: *mut _xmlNode,
1836    schema: &RelaxNgSchema,
1837    ctxt: &mut RelaxNgValidCtxt,
1838) -> bool {
1839    unsafe {
1840        if node.is_null() || pattern.children.is_empty() {
1841            ctxt.record_error(format!(
1842                "Expected at least one matching element for oneOrMore at '{}'",
1843                ctxt.current_path()
1844            ));
1845            return false;
1846        }
1847
1848        let child_pat = &pattern.children[0];
1849        let mut matched = false;
1850        let mut valid = true;
1851
1852        // Match at least one child, then as many as possible
1853        let mut child = (*node).children;
1854        while !child.is_null() {
1855            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1856                let saved_errors = ctxt.errors.len();
1857                let saved_nb = ctxt.nb_errors;
1858
1859                if rng_validate_pattern(child_pat, child, schema, ctxt) {
1860                    matched = true;
1861                } else {
1862                    // Restore errors and stop
1863                    ctxt.errors.truncate(saved_errors);
1864                    ctxt.nb_errors = saved_nb;
1865                    break;
1866                }
1867            }
1868            child = (*child).next;
1869        }
1870
1871        if !matched {
1872            ctxt.record_error(format!(
1873                "Expected at least one matching element for oneOrMore at '{}'",
1874                ctxt.current_path()
1875            ));
1876            valid = false;
1877        }
1878
1879        valid
1880    }
1881}
1882
1883/// Validate an optional pattern.
1884///
1885/// # SAFETY
1886///
1887/// - `node` must be a valid pointer to an _xmlNode or NULL.
1888fn rng_validate_optional_pattern(
1889    pattern: &RelaxNgPattern,
1890    node: *mut _xmlNode,
1891    schema: &RelaxNgSchema,
1892    ctxt: &mut RelaxNgValidCtxt,
1893) -> bool {
1894    {
1895        if pattern.children.is_empty() {
1896            return true;
1897        }
1898
1899        // Optional means the pattern can match or not — both are valid
1900        let child_pat = &pattern.children[0];
1901        let saved_errors = ctxt.errors.len();
1902        let saved_nb = ctxt.nb_errors;
1903
1904        let result = rng_validate_pattern(child_pat, node, schema, ctxt);
1905
1906        if !result {
1907            // Restore errors — it's okay that optional didn't match
1908            ctxt.errors.truncate(saved_errors);
1909            ctxt.nb_errors = saved_nb;
1910        }
1911
1912        true // Optional always returns true
1913    }
1914}
1915
1916/// Validate a list pattern.
1917///
1918/// # SAFETY
1919///
1920/// - `node` must be a valid pointer to an _xmlNode or NULL.
1921fn rng_validate_list_pattern(
1922    pattern: &RelaxNgPattern,
1923    node: *mut _xmlNode,
1924    _schema: &RelaxNgSchema,
1925    _ctxt: &mut RelaxNgValidCtxt,
1926) -> bool {
1927    unsafe {
1928        if node.is_null() {
1929            return pattern.children.is_empty();
1930        }
1931
1932        // List pattern: whitespace-separated tokens, each matching the child pattern
1933        let text = get_node_text(node);
1934        if text.trim().is_empty() {
1935            return true;
1936        }
1937
1938        let tokens: Vec<&str> = text.split_whitespace().collect();
1939        let mut valid = true;
1940
1941        for token in &tokens {
1942            // For each token, validate against the child pattern
1943            // We do a simplified check — just ensure it's non-empty
1944            if token.is_empty() {
1945                valid = false;
1946                break;
1947            }
1948        }
1949
1950        valid
1951    }
1952}
1953
1954/// Validate a group pattern.
1955///
1956/// # SAFETY
1957///
1958/// - `node` must be a valid pointer to an _xmlNode or NULL.
1959fn rng_validate_group_pattern(
1960    pattern: &RelaxNgPattern,
1961    node: *mut _xmlNode,
1962    schema: &RelaxNgSchema,
1963    ctxt: &mut RelaxNgValidCtxt,
1964) -> bool {
1965    {
1966        // Group is similar to sequence — validate children in order
1967        rng_validate_sequence_pattern(pattern, node, schema, ctxt)
1968    }
1969}
1970
1971/// Validate a data pattern.
1972///
1973/// # SAFETY
1974///
1975/// - `node` must be a valid pointer to an _xmlNode or NULL.
1976fn rng_validate_data_pattern(
1977    pattern: &RelaxNgPattern,
1978    node: *mut _xmlNode,
1979    ctxt: &mut RelaxNgValidCtxt,
1980) -> bool {
1981    unsafe {
1982        if node.is_null() {
1983            return false;
1984        }
1985
1986        let text = get_node_text(node);
1987        let datatype = pattern.datatype.as_deref();
1988
1989        if !rng_validate_datatype_value(datatype, &text) {
1990            ctxt.record_error(format!(
1991                "Value '{}' does not match datatype '{:?}' at '{}'",
1992                text,
1993                datatype,
1994                ctxt.current_path()
1995            ));
1996            return false;
1997        }
1998
1999        true
2000    }
2001}
2002
2003/// Validate a value pattern.
2004///
2005/// # SAFETY
2006///
2007/// - `node` must be a valid pointer to an _xmlNode or NULL.
2008fn rng_validate_value_pattern(
2009    pattern: &RelaxNgPattern,
2010    node: *mut _xmlNode,
2011    ctxt: &mut RelaxNgValidCtxt,
2012) -> bool {
2013    unsafe {
2014        if node.is_null() {
2015            return false;
2016        }
2017
2018        let text = get_node_text(node).trim().to_string();
2019        let expected = pattern.value.as_deref().unwrap_or("");
2020
2021        if text != expected {
2022            ctxt.record_error(format!(
2023                "Value '{}' does not match expected value '{}' at '{}'",
2024                text,
2025                expected,
2026                ctxt.current_path()
2027            ));
2028            return false;
2029        }
2030
2031        true
2032    }
2033}
2034
2035/// Validate a ref pattern.
2036///
2037/// # SAFETY
2038///
2039/// - `node` must be a valid pointer to an _xmlNode or NULL.
2040fn rng_validate_ref_pattern(
2041    pattern: &RelaxNgPattern,
2042    node: *mut _xmlNode,
2043    schema: &RelaxNgSchema,
2044    ctxt: &mut RelaxNgValidCtxt,
2045) -> bool {
2046    {
2047        let ref_name = pattern.name.as_deref().unwrap_or("");
2048
2049        if ref_name.is_empty() {
2050            ctxt.record_error("Ref pattern has no name".to_string());
2051            return false;
2052        }
2053
2054        // Look up the definition
2055        match schema.grammar.lookup(ref_name) {
2056            Some(def_pattern) => {
2057                // Validate against the definition's pattern
2058                rng_validate_pattern(def_pattern, node, schema, ctxt)
2059            }
2060            None => {
2061                ctxt.record_error(format!(
2062                    "Undefined reference '{}' at '{}'",
2063                    ref_name,
2064                    ctxt.current_path()
2065                ));
2066                false
2067            }
2068        }
2069    }
2070}
2071
2072// ═══════════════════════════════════════════════════════════════════════════════
2073// Datatype Validation for RELAX NG
2074// ═══════════════════════════════════════════════════════════════════════════════
2075
2076/// Validate a value against a RELAX NG datatype.
2077///
2078/// RELAX NG supports a subset of XML Schema datatypes. This function
2079/// provides basic datatype validation for common types.
2080fn rng_validate_datatype_value(datatype: Option<&str>, value: &str) -> bool {
2081    let dt = match datatype {
2082        Some(d) => d,
2083        None => return true, // No datatype specified — accept anything
2084    };
2085
2086    match dt {
2087        "string" | "token" => true,
2088        "boolean" => {
2089            matches!(value, "true" | "false" | "1" | "0")
2090        }
2091        "integer" | "int" | "short" | "byte" | "long" => {
2092            if value.is_empty() {
2093                return false;
2094            }
2095            let trimmed = if value.starts_with('+') || value.starts_with('-') {
2096                &value[1..]
2097            } else {
2098                value
2099            };
2100            !trimmed.is_empty() && trimmed.chars().all(|c| c.is_ascii_digit())
2101        }
2102        "decimal" | "double" | "float" => {
2103            if value.is_empty() {
2104                return false;
2105            }
2106            // Allow INF, -INF, NaN for float/double
2107            if matches!(dt, "float" | "double") && matches!(value, "INF" | "-INF" | "NaN") {
2108                return true;
2109            }
2110            value.parse::<f64>().is_ok()
2111        }
2112        "NCName" | "Name" | "ID" | "IDREF" | "NMTOKEN" => {
2113            !value.is_empty() && !value.starts_with(|c: char| c.is_ascii_digit())
2114        }
2115        "anyURI" => {
2116            // Simple URI validation — non-empty and no spaces
2117            !value.is_empty() && !value.contains(char::is_whitespace)
2118        }
2119        "QName" => {
2120            if value.is_empty() {
2121                return false;
2122            }
2123            if let Some(pos) = value.find(':') {
2124                pos > 0 && pos < value.len() - 1
2125            } else {
2126                true
2127            }
2128        }
2129        _ => {
2130            // Unknown datatype — accept by default
2131            // This matches libxml2's lenient behavior
2132            true
2133        }
2134    }
2135}
2136
2137// ═══════════════════════════════════════════════════════════════════════════════
2138// Public API Functions
2139// ═══════════════════════════════════════════════════════════════════════════════
2140
2141/// Parse a RELAX NG schema from an XML string.
2142///
2143/// Returns the parsed schema, or an error message on failure.
2144pub fn rng_parse_schema(xml_doc: &str) -> Result<RelaxNgSchema, String> {
2145    rng_parse(xml_doc)
2146}
2147
2148/// Parse a RELAX NG schema from a parsed XML document.
2149///
2150/// # SAFETY
2151///
2152/// - `doc` must be a valid pointer to an _xmlDoc.
2153pub unsafe fn rng_parse_schema_doc(doc: *mut _xmlDoc) -> Result<RelaxNgSchema, String> {
2154    rng_parse_doc(doc)
2155}
2156
2157/// Validate a document against a RELAX NG schema.
2158///
2159/// Returns `true` if the document is valid.
2160///
2161/// # SAFETY
2162///
2163/// - `doc` must be a valid pointer to an _xmlDoc or NULL.
2164pub unsafe fn rng_validate_doc_schema(
2165    schema: &RelaxNgSchema,
2166    doc: *mut _xmlDoc,
2167    ctxt: &mut RelaxNgValidCtxt,
2168) -> bool {
2169    rng_validate_doc(schema, doc, ctxt)
2170}
2171
2172// ═══════════════════════════════════════════════════════════════════════════════
2173// C ABI Functions
2174// ═══════════════════════════════════════════════════════════════════════════════
2175
2176// These are the C-compatible entry points that get exported via the ABI layer.
2177// They use raw pointers and follow libxml2's calling conventions.
2178
2179/// Create a new RELAX NG parser context.
2180///
2181/// # UPSTREAM-PARITY
2182///
2183/// ```c
2184/// xmlRelaxNGParserCtxtPtr xmlRelaxNGNewParserCtxt(const char *URL);
2185/// ```
2186///
2187/// # SAFETY
2188///
2189/// - `url` must be a valid null-terminated C string or NULL.
2190#[no_mangle]
2191pub unsafe extern "C" fn xmlRelaxNGNewParserCtxt(url: *const c_char) -> *mut c_void {
2192    if url.is_null() {
2193        let ctxt = allocator::xmlMallocZero(size_of::<RelaxNgSchema>() as usize);
2194        return ctxt;
2195    }
2196
2197    let url_str = unsafe {
2198        let mut len = 0;
2199        while *url.add(len) != 0 {
2200            len += 1;
2201        }
2202        let slice = std::slice::from_raw_parts(url as *const u8, len);
2203        String::from_utf8_lossy(slice).to_string()
2204    };
2205
2206    // Try to parse the schema from the URL
2207    if !url_str.is_empty() {
2208        let url_c = std::ffi::CString::new(url_str.clone()).ok();
2209        if let Some(c) = url_c {
2210            let doc = crate::abi::exports_xml2::xmlParseFile(c.as_ptr());
2211            if !doc.is_null() {
2212                let result = rng_parse_doc(doc);
2213                crate::abi::exports_xml2::xmlFreeDoc(doc);
2214                if let Ok(schema) = result {
2215                    let schema_box = Box::new(schema);
2216                    return Box::into_raw(schema_box) as *mut c_void;
2217                }
2218            }
2219        }
2220    }
2221
2222    // Return empty context for later parsing
2223
2224    allocator::xmlMallocZero(size_of::<RelaxNgSchema>() as usize)
2225}
2226
2227/// Create a new RELAX NG parser context from a memory buffer.
2228///
2229/// # UPSTREAM-PARITY
2230///
2231/// ```c
2232/// xmlRelaxNGParserCtxtPtr xmlRelaxNGNewMemParserCtxt(const char *buffer, int size);
2233/// ```
2234///
2235/// # SAFETY
2236///
2237/// - `buffer` must be a valid pointer to a buffer of at least `size` bytes.
2238#[no_mangle]
2239pub unsafe extern "C" fn xmlRelaxNGNewMemParserCtxt(
2240    buffer: *const c_char,
2241    size: c_int,
2242) -> *mut c_void {
2243    if buffer.is_null() || size <= 0 {
2244        return ptr::null_mut();
2245    }
2246
2247    // Parse the schema immediately
2248    let buf_slice = unsafe { std::slice::from_raw_parts(buffer as *const u8, size as usize) };
2249    let xml_str = String::from_utf8_lossy(buf_slice).to_string();
2250
2251    match rng_parse(&xml_str) {
2252        Ok(schema) => {
2253            let schema_box = Box::new(schema);
2254            Box::into_raw(schema_box) as *mut c_void
2255        }
2256        Err(_) => ptr::null_mut(),
2257    }
2258}
2259
2260/// Parse a RELAX NG schema.
2261///
2262/// # UPSTREAM-PARITY
2263///
2264/// ```c
2265/// xmlRelaxNGPtr xmlRelaxNGParse(xmlRelaxNGParserCtxtPtr ctxt);
2266/// ```
2267///
2268/// # SAFETY
2269///
2270/// - `ctxt` must be a valid pointer to a parser context, or NULL.
2271#[no_mangle]
2272pub const unsafe extern "C" fn xmlRelaxNGParse(ctxt: *mut c_void) -> *mut c_void {
2273    if ctxt.is_null() {
2274        return ptr::null_mut();
2275    }
2276
2277    // If the context already contains a parsed schema (from xmlRelaxNGNewMemParserCtxt),
2278    // return it. Otherwise, return the context as-is.
2279    ctxt
2280}
2281
2282/// Free a RELAX NG schema.
2283///
2284/// # UPSTREAM-PARITY
2285///
2286/// ```c
2287/// void xmlRelaxNGFree(xmlRelaxNGPtr schema);
2288/// ```
2289///
2290/// # SAFETY
2291///
2292/// - `schema` must be a valid pointer to a schema, or NULL.
2293#[no_mangle]
2294pub unsafe extern "C" fn xmlRelaxNGFree(schema: *mut c_void) {
2295    if schema.is_null() {
2296        return;
2297    }
2298    // SAFETY: Reconstruct the Box to drop it.
2299    unsafe {
2300        let _ = Box::from_raw(schema as *mut RelaxNgSchema);
2301    }
2302}
2303
2304/// Free a RELAX NG parser context.
2305///
2306/// # UPSTREAM-PARITY
2307///
2308/// ```c
2309/// void xmlRelaxNGFreeParserCtxt(xmlRelaxNGParserCtxtPtr ctxt);
2310/// ```
2311///
2312/// # SAFETY
2313///
2314/// - `ctxt` must be a valid pointer to a parser context, or NULL.
2315#[no_mangle]
2316pub unsafe extern "C" fn xmlRelaxNGFreeParserCtxt(ctxt: *mut c_void) {
2317    if ctxt.is_null() {
2318        return;
2319    }
2320    // SAFETY: Reconstruct the Box to drop it.
2321    unsafe {
2322        let _ = Box::from_raw(ctxt as *mut RelaxNgSchema);
2323    }
2324}
2325
2326/// Create a new RELAX NG validation context.
2327///
2328/// # UPSTREAM-PARITY
2329///
2330/// ```c
2331/// xmlRelaxNGValidCtxtPtr xmlRelaxNGNewValidCtxt(xmlRelaxNGPtr schema);
2332/// ```
2333///
2334/// # SAFETY
2335///
2336/// - `schema` must be a valid pointer to a schema, or NULL.
2337#[no_mangle]
2338pub unsafe extern "C" fn xmlRelaxNGNewValidCtxt(schema: *mut c_void) -> *mut c_void {
2339    let mut ctxt = RelaxNgValidCtxt::new();
2340
2341    if !schema.is_null() {
2342        // SAFETY: The schema pointer is assumed to be a valid RelaxNgSchema.
2343        unsafe {
2344            let schema_ref = &*(schema as *const RelaxNgSchema);
2345            ctxt.schema = Some(schema_ref.clone());
2346        }
2347    }
2348
2349    let boxed = Box::new(ctxt);
2350    Box::into_raw(boxed) as *mut c_void
2351}
2352
2353/// Free a RELAX NG validation context.
2354///
2355/// # UPSTREAM-PARITY
2356///
2357/// ```c
2358/// void xmlRelaxNGFreeValidCtxt(xmlRelaxNGValidCtxtPtr ctxt);
2359/// ```
2360///
2361/// # SAFETY
2362///
2363/// - `ctxt` must be a valid pointer to a validation context, or NULL.
2364#[no_mangle]
2365pub unsafe extern "C" fn xmlRelaxNGFreeValidCtxt(ctxt: *mut c_void) {
2366    if ctxt.is_null() {
2367        return;
2368    }
2369    // SAFETY: Reconstruct the Box to drop it.
2370    unsafe {
2371        let _ = Box::from_raw(ctxt as *mut RelaxNgValidCtxt);
2372    }
2373}
2374
2375/// Validate a document against a RELAX NG schema.
2376///
2377/// # UPSTREAM-PARITY
2378///
2379/// ```c
2380/// int xmlRelaxNGValidateDoc(xmlRelaxNGValidCtxtPtr ctxt, xmlDocPtr doc);
2381/// ```
2382///
2383/// Returns 0 if valid, -1 on internal error, or the number of validation errors.
2384///
2385/// # SAFETY
2386///
2387/// - `ctxt` must be a valid pointer to a validation context, or NULL.
2388/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
2389#[no_mangle]
2390pub unsafe extern "C" fn xmlRelaxNGValidateDoc(ctxt: *mut c_void, doc: *mut _xmlDoc) -> c_int {
2391    if ctxt.is_null() || doc.is_null() {
2392        return -1;
2393    }
2394
2395    unsafe {
2396        let valid_ctxt = &mut *(ctxt as *mut RelaxNgValidCtxt);
2397        let schema = match &valid_ctxt.schema {
2398            Some(s) => s,
2399            None => return -1,
2400        };
2401
2402        let mut temp_ctxt = RelaxNgValidCtxt::new();
2403
2404        let valid = rng_validate_doc(schema, doc, &mut temp_ctxt);
2405
2406        if valid {
2407            0
2408        } else {
2409            valid_ctxt.errors = temp_ctxt.errors;
2410            valid_ctxt.nb_errors = temp_ctxt.nb_errors;
2411            temp_ctxt.nb_errors
2412        }
2413    }
2414}
2415
2416/// Validate a full element against a RELAX NG schema.
2417///
2418/// # UPSTREAM-PARITY
2419///
2420/// ```c
2421/// int xmlRelaxNGValidateFullElement(xmlRelaxNGValidCtxtPtr ctxt,
2422///                                    xmlDocPtr doc,
2423///                                    xmlNodePtr elem);
2424/// ```
2425///
2426/// Returns 0 if valid, -1 on internal error, or the number of validation errors.
2427///
2428/// # SAFETY
2429///
2430/// - `ctxt` must be a valid pointer to a validation context, or NULL.
2431/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
2432/// - `elem` must be a valid pointer to an element node, or NULL.
2433#[no_mangle]
2434pub unsafe extern "C" fn xmlRelaxNGValidateFullElement(
2435    ctxt: *mut c_void,
2436    doc: *mut _xmlDoc,
2437    elem: *mut _xmlNode,
2438) -> c_int {
2439    if ctxt.is_null() || doc.is_null() || elem.is_null() {
2440        return -1;
2441    }
2442
2443    unsafe {
2444        let valid_ctxt = &mut *(ctxt as *mut RelaxNgValidCtxt);
2445        let schema = match &valid_ctxt.schema {
2446            Some(s) => s,
2447            None => return -1,
2448        };
2449
2450        let mut temp_ctxt = RelaxNgValidCtxt::new();
2451        temp_ctxt.path = valid_ctxt.path.clone();
2452
2453        let start_pattern = match &schema.grammar.start {
2454            Some(p) => p,
2455            None => return -1,
2456        };
2457
2458        let valid = rng_validate_pattern(start_pattern, elem, schema, &mut temp_ctxt);
2459
2460        if valid {
2461            0
2462        } else {
2463            valid_ctxt.errors = temp_ctxt.errors;
2464            valid_ctxt.nb_errors = temp_ctxt.nb_errors;
2465            temp_ctxt.nb_errors
2466        }
2467    }
2468}
2469
2470// ═══════════════════════════════════════════════════════════════════════════════
2471// Tests
2472// ═══════════════════════════════════════════════════════════════════════════════
2473
2474#[cfg(test)]
2475mod tests {
2476    use super::*;
2477
2478    // ── Name Class Tests ───────────────────────────────────────────────────
2479
2480    #[test]
2481    fn test_name_class_name() {
2482        let nc = RelaxNgNameClass::Name("foo".to_string());
2483        assert!(nc.matches("foo", None));
2484        assert!(!nc.matches("bar", None));
2485        assert!(!nc.matches("FOO", None));
2486    }
2487
2488    #[test]
2489    fn test_name_class_any_name() {
2490        let nc = RelaxNgNameClass::AnyName;
2491        assert!(nc.matches("foo", None));
2492        assert!(nc.matches("bar", None));
2493        assert!(nc.matches("anything", Some("urn:ns")));
2494    }
2495
2496    #[test]
2497    fn test_name_class_ns_name() {
2498        let nc = RelaxNgNameClass::NsName("urn:example".to_string());
2499        assert!(nc.matches("foo", Some("urn:example")));
2500        assert!(!nc.matches("foo", Some("urn:other")));
2501        assert!(!nc.matches("foo", None));
2502    }
2503
2504    #[test]
2505    fn test_name_class_choice() {
2506        let nc = RelaxNgNameClass::Choice(vec![
2507            RelaxNgNameClass::Name("a".to_string()),
2508            RelaxNgNameClass::Name("b".to_string()),
2509        ]);
2510        assert!(nc.matches("a", None));
2511        assert!(nc.matches("b", None));
2512        assert!(!nc.matches("c", None));
2513    }
2514
2515    #[test]
2516    fn test_name_class_except() {
2517        let nc = RelaxNgNameClass::Except(
2518            Box::new(RelaxNgNameClass::AnyName),
2519            Box::new(RelaxNgNameClass::Name("bad".to_string())),
2520        );
2521        assert!(nc.matches("good", None));
2522        assert!(!nc.matches("bad", None));
2523    }
2524
2525    // ── Schema Parsing Tests ──────────────────────────────────────────────
2526
2527    #[test]
2528    fn test_parse_simple_element_schema() {
2529        let schema_xml = r#"<?xml version="1.0"?>
2530<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
2531  <text/>
2532</element>"#;
2533
2534        let result = rng_parse(schema_xml);
2535        assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
2536        let schema = result.unwrap();
2537        assert!(schema.grammar.start.is_some());
2538        if let Some(ref start) = schema.grammar.start {
2539            assert_eq!(start.pattern_type, RelaxNgPatternType::Element);
2540            assert_eq!(start.name.as_deref(), Some("root"));
2541        }
2542    }
2543
2544    #[test]
2545    fn test_parse_grammar_schema() {
2546        let schema_xml = r#"<?xml version="1.0"?>
2547<grammar xmlns="http://relaxng.org/ns/structure/1.0">
2548  <start>
2549    <element name="root">
2550      <text/>
2551    </element>
2552  </start>
2553</grammar>"#;
2554
2555        let result = rng_parse(schema_xml);
2556        assert!(
2557            result.is_ok(),
2558            "Failed to parse grammar: {:?}",
2559            result.err()
2560        );
2561        let schema = result.unwrap();
2562        assert!(schema.grammar.start.is_some());
2563    }
2564
2565    #[test]
2566    fn test_parse_with_define_and_ref() {
2567        let schema_xml = r#"<?xml version="1.0"?>
2568<grammar xmlns="http://relaxng.org/ns/structure/1.0">
2569  <define name="textBlock">
2570    <text/>
2571  </define>
2572  <start>
2573    <element name="doc">
2574      <ref name="textBlock"/>
2575    </element>
2576  </start>
2577</grammar>"#;
2578
2579        let result = rng_parse(schema_xml);
2580        assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
2581        let schema = result.unwrap();
2582        assert_eq!(schema.grammar.defines.len(), 1);
2583        assert_eq!(schema.grammar.defines[0].name, "textBlock");
2584        assert!(schema.grammar.start.is_some());
2585    }
2586
2587    #[test]
2588    fn test_parse_choice_schema() {
2589        let schema_xml = r#"<?xml version="1.0"?>
2590<grammar xmlns="http://relaxng.org/ns/structure/1.0">
2591  <start>
2592    <choice>
2593      <element name="a">
2594        <text/>
2595      </element>
2596      <element name="b">
2597        <text/>
2598      </element>
2599    </choice>
2600  </start>
2601</grammar>"#;
2602
2603        let result = rng_parse(schema_xml);
2604        assert!(result.is_ok(), "Failed to parse choice: {:?}", result.err());
2605    }
2606
2607    #[test]
2608    fn test_parse_attribute_schema() {
2609        let schema_xml = r#"<?xml version="1.0"?>
2610<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
2611  <attribute name="attr1">
2612    <text/>
2613  </attribute>
2614  <text/>
2615</element>"#;
2616
2617        let result = rng_parse(schema_xml);
2618        assert!(
2619            result.is_ok(),
2620            "Failed to parse attribute: {:?}",
2621            result.err()
2622        );
2623    }
2624
2625    #[test]
2626    fn test_parse_empty_document_fails() {
2627        let result = rng_parse("");
2628        assert!(result.is_err());
2629    }
2630
2631    #[test]
2632    fn test_parse_invalid_xml_fails() {
2633        let result = rng_parse("not valid xml <<<");
2634        assert!(result.is_err());
2635    }
2636
2637    // ── Validation Tests ──────────────────────────────────────────────────
2638
2639    /// Validates a document against a schema with a single `element` pattern.
2640    ///
2641    /// # Safety
2642    ///
2643    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
2644    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
2645    ///   the pointer is asserted non-NULL before `rng_validate_doc`
2646    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
2647    #[test]
2648    fn test_validate_simple_element() {
2649        let schema_xml = r#"<?xml version="1.0"?>
2650<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
2651  <text/>
2652</element>"#;
2653
2654        let doc_xml = r#"<?xml version="1.0"?>
2655<root>Hello</root>"#;
2656
2657        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
2658
2659        let doc = unsafe {
2660            crate::abi::exports_xml2::xmlReadMemory(
2661                doc_xml.as_ptr() as *const c_char,
2662                doc_xml.len() as c_int,
2663                c"test.xml".as_ptr() as *const c_char,
2664                ptr::null(),
2665                0,
2666            )
2667        };
2668        assert!(!doc.is_null(), "Failed to parse document");
2669
2670        let mut ctxt = RelaxNgValidCtxt::new();
2671        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
2672        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2673
2674        assert!(valid, "Validation failed: {:?}", ctxt.errors);
2675    }
2676
2677    /// Verifies validation fails when the root element name does not match.
2678    ///
2679    /// # Safety
2680    ///
2681    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
2682    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
2683    ///   the pointer is asserted non-NULL before `rng_validate_doc`
2684    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
2685    #[test]
2686    fn test_validate_element_mismatch() {
2687        let schema_xml = r#"<?xml version="1.0"?>
2688<element name="expected" xmlns="http://relaxng.org/ns/structure/1.0">
2689  <text/>
2690</element>"#;
2691
2692        let doc_xml = r#"<?xml version="1.0"?>
2693<actual>Content</actual>"#;
2694
2695        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
2696
2697        let doc = unsafe {
2698            crate::abi::exports_xml2::xmlReadMemory(
2699                doc_xml.as_ptr() as *const c_char,
2700                doc_xml.len() as c_int,
2701                c"test.xml".as_ptr() as *const c_char,
2702                ptr::null(),
2703                0,
2704            )
2705        };
2706        assert!(!doc.is_null());
2707
2708        let mut ctxt = RelaxNgValidCtxt::new();
2709        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
2710        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2711
2712        assert!(!valid, "Validation should have failed");
2713        assert!(ctxt.nb_errors > 0);
2714    }
2715
2716    /// Validates a document whose element carries a declared attribute.
2717    ///
2718    /// # Safety
2719    ///
2720    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
2721    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
2722    ///   the pointer is asserted non-NULL before `rng_validate_doc`
2723    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
2724    #[test]
2725    fn test_validate_with_attribute() {
2726        let schema_xml = r#"<?xml version="1.0"?>
2727<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
2728  <attribute name="id">
2729    <text/>
2730  </attribute>
2731  <text/>
2732</element>"#;
2733
2734        let doc_xml = r#"<?xml version="1.0"?>
2735<root id="x1">Content</root>"#;
2736
2737        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
2738
2739        let doc = unsafe {
2740            crate::abi::exports_xml2::xmlReadMemory(
2741                doc_xml.as_ptr() as *const c_char,
2742                doc_xml.len() as c_int,
2743                c"test.xml".as_ptr() as *const c_char,
2744                ptr::null(),
2745                0,
2746            )
2747        };
2748        assert!(!doc.is_null());
2749
2750        let mut ctxt = RelaxNgValidCtxt::new();
2751        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
2752        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2753
2754        assert!(valid, "Validation failed: {:?}", ctxt.errors);
2755    }
2756
2757    /// Verifies validation fails when a required attribute is absent.
2758    ///
2759    /// # Safety
2760    ///
2761    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
2762    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
2763    ///   the pointer is asserted non-NULL before `rng_validate_doc`
2764    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
2765    #[test]
2766    fn test_validate_missing_attribute() {
2767        let schema_xml = r#"<?xml version="1.0"?>
2768<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
2769  <attribute name="required">
2770    <text/>
2771  </attribute>
2772  <text/>
2773</element>"#;
2774
2775        let doc_xml = r#"<?xml version="1.0"?>
2776<root>Content</root>"#;
2777
2778        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
2779
2780        let doc = unsafe {
2781            crate::abi::exports_xml2::xmlReadMemory(
2782                doc_xml.as_ptr() as *const c_char,
2783                doc_xml.len() as c_int,
2784                c"test.xml".as_ptr() as *const c_char,
2785                ptr::null(),
2786                0,
2787            )
2788        };
2789        assert!(!doc.is_null());
2790
2791        let mut ctxt = RelaxNgValidCtxt::new();
2792        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
2793        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2794
2795        assert!(
2796            !valid,
2797            "Validation should have failed for missing attribute"
2798        );
2799    }
2800
2801    /// Validates a document matching one branch of a `choice` pattern.
2802    ///
2803    /// # Safety
2804    ///
2805    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
2806    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
2807    ///   the pointer is asserted non-NULL before `rng_validate_doc`
2808    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
2809    #[test]
2810    fn test_validate_with_choice() {
2811        let schema_xml = r#"<?xml version="1.0"?>
2812<grammar xmlns="http://relaxng.org/ns/structure/1.0">
2813  <start>
2814    <choice>
2815      <element name="a">
2816        <text/>
2817      </element>
2818      <element name="b">
2819        <text/>
2820      </element>
2821    </choice>
2822  </start>
2823</grammar>"#;
2824
2825        let doc_xml = r#"<?xml version="1.0"?>
2826<a>First choice</a>"#;
2827
2828        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
2829
2830        let doc = unsafe {
2831            crate::abi::exports_xml2::xmlReadMemory(
2832                doc_xml.as_ptr() as *const c_char,
2833                doc_xml.len() as c_int,
2834                c"test.xml".as_ptr() as *const c_char,
2835                ptr::null(),
2836                0,
2837            )
2838        };
2839        assert!(!doc.is_null());
2840
2841        let mut ctxt = RelaxNgValidCtxt::new();
2842        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
2843        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2844
2845        assert!(valid, "Choice validation failed: {:?}", ctxt.errors);
2846    }
2847
2848    /// Verifies validation fails when no `choice` branch matches.
2849    ///
2850    /// # Safety
2851    ///
2852    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
2853    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
2854    ///   the pointer is asserted non-NULL before `rng_validate_doc`
2855    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
2856    #[test]
2857    fn test_validate_choice_no_match() {
2858        let schema_xml = r#"<?xml version="1.0"?>
2859<grammar xmlns="http://relaxng.org/ns/structure/1.0">
2860  <start>
2861    <choice>
2862      <element name="a">
2863        <text/>
2864      </element>
2865      <element name="b">
2866        <text/>
2867      </element>
2868    </choice>
2869  </start>
2870</grammar>"#;
2871
2872        let doc_xml = r#"<?xml version="1.0"?>
2873<c>Neither choice</c>"#;
2874
2875        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
2876
2877        let doc = unsafe {
2878            crate::abi::exports_xml2::xmlReadMemory(
2879                doc_xml.as_ptr() as *const c_char,
2880                doc_xml.len() as c_int,
2881                c"test.xml".as_ptr() as *const c_char,
2882                ptr::null(),
2883                0,
2884            )
2885        };
2886        assert!(!doc.is_null());
2887
2888        let mut ctxt = RelaxNgValidCtxt::new();
2889        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
2890        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2891
2892        assert!(
2893            !valid,
2894            "Validation should have failed for no matching choice"
2895        );
2896    }
2897
2898    /// Validates a grammar that uses a named `ref` to a `define`.
2899    ///
2900    /// # Safety
2901    ///
2902    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
2903    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
2904    ///   the pointer is asserted non-NULL before `rng_validate_doc`
2905    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
2906    #[test]
2907    fn test_validate_grammar_with_ref() {
2908        let schema_xml = r#"<?xml version="1.0"?>
2909<grammar xmlns="http://relaxng.org/ns/structure/1.0">
2910  <define name="para">
2911    <element name="p">
2912      <text/>
2913    </element>
2914  </define>
2915  <start>
2916    <element name="doc">
2917      <zeroOrMore>
2918        <ref name="para"/>
2919      </zeroOrMore>
2920    </element>
2921  </start>
2922</grammar>"#;
2923
2924        let doc_xml = r#"<?xml version="1.0"?>
2925<doc><p>First</p><p>Second</p></doc>"#;
2926
2927        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
2928
2929        let doc = unsafe {
2930            crate::abi::exports_xml2::xmlReadMemory(
2931                doc_xml.as_ptr() as *const c_char,
2932                doc_xml.len() as c_int,
2933                c"test.xml".as_ptr() as *const c_char,
2934                ptr::null(),
2935                0,
2936            )
2937        };
2938        assert!(!doc.is_null());
2939
2940        let mut ctxt = RelaxNgValidCtxt::new();
2941        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
2942        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2943
2944        assert!(valid, "Ref validation failed: {:?}", ctxt.errors);
2945    }
2946
2947    /// Validates repeated children under a `zeroOrMore` pattern.
2948    ///
2949    /// # Safety
2950    ///
2951    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
2952    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
2953    ///   the pointer is asserted non-NULL before `rng_validate_doc`
2954    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
2955    #[test]
2956    fn test_validate_zero_or_more() {
2957        let schema_xml = r#"<?xml version="1.0"?>
2958<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
2959  <zeroOrMore>
2960    <element name="item">
2961      <text/>
2962    </element>
2963  </zeroOrMore>
2964</element>"#;
2965
2966        let doc_xml = r#"<?xml version="1.0"?>
2967<root><item>A</item><item>B</item></root>"#;
2968
2969        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
2970
2971        let doc = unsafe {
2972            crate::abi::exports_xml2::xmlReadMemory(
2973                doc_xml.as_ptr() as *const c_char,
2974                doc_xml.len() as c_int,
2975                c"test.xml".as_ptr() as *const c_char,
2976                ptr::null(),
2977                0,
2978            )
2979        };
2980        assert!(!doc.is_null());
2981
2982        let mut ctxt = RelaxNgValidCtxt::new();
2983        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
2984        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2985
2986        assert!(valid, "zeroOrMore validation failed: {:?}", ctxt.errors);
2987    }
2988
2989    /// Verifies a `zeroOrMore` pattern accepts zero repetitions.
2990    ///
2991    /// # Safety
2992    ///
2993    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
2994    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
2995    ///   the pointer is asserted non-NULL before `rng_validate_doc`
2996    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
2997    #[test]
2998    fn test_validate_zero_or_more_empty() {
2999        let schema_xml = r#"<?xml version="1.0"?>
3000<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
3001  <zeroOrMore>
3002    <element name="item">
3003      <text/>
3004    </element>
3005  </zeroOrMore>
3006</element>"#;
3007
3008        let doc_xml = r#"<?xml version="1.0"?>
3009<root></root>"#;
3010
3011        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3012
3013        let doc = unsafe {
3014            crate::abi::exports_xml2::xmlReadMemory(
3015                doc_xml.as_ptr() as *const c_char,
3016                doc_xml.len() as c_int,
3017                c"test.xml".as_ptr() as *const c_char,
3018                ptr::null(),
3019                0,
3020            )
3021        };
3022        assert!(!doc.is_null());
3023
3024        let mut ctxt = RelaxNgValidCtxt::new();
3025        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3026        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3027
3028        assert!(valid, "Empty zeroOrMore should be valid");
3029    }
3030
3031    /// Validates a document with one occurrence under `oneOrMore`.
3032    ///
3033    /// # Safety
3034    ///
3035    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3036    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3037    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3038    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3039    #[test]
3040    fn test_validate_one_or_more() {
3041        let schema_xml = r#"<?xml version="1.0"?>
3042<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
3043  <oneOrMore>
3044    <element name="item">
3045      <text/>
3046    </element>
3047  </oneOrMore>
3048</element>"#;
3049
3050        let doc_xml = r#"<?xml version="1.0"?>
3051<root><item>Single</item></root>"#;
3052
3053        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3054
3055        let doc = unsafe {
3056            crate::abi::exports_xml2::xmlReadMemory(
3057                doc_xml.as_ptr() as *const c_char,
3058                doc_xml.len() as c_int,
3059                c"test.xml".as_ptr() as *const c_char,
3060                ptr::null(),
3061                0,
3062            )
3063        };
3064        assert!(!doc.is_null());
3065
3066        let mut ctxt = RelaxNgValidCtxt::new();
3067        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3068        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3069
3070        assert!(valid, "oneOrMore validation failed: {:?}", ctxt.errors);
3071    }
3072
3073    /// Validates an `optional` element that is present.
3074    ///
3075    /// # Safety
3076    ///
3077    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3078    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3079    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3080    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3081    #[test]
3082    fn test_validate_optional_present() {
3083        let schema_xml = r#"<?xml version="1.0"?>
3084<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
3085  <optional>
3086    <element name="opt">
3087      <text/>
3088    </element>
3089  </optional>
3090  <text/>
3091</element>"#;
3092
3093        let doc_xml = r#"<?xml version="1.0"?>
3094<root><opt>present</opt>text</root>"#;
3095
3096        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3097
3098        let doc = unsafe {
3099            crate::abi::exports_xml2::xmlReadMemory(
3100                doc_xml.as_ptr() as *const c_char,
3101                doc_xml.len() as c_int,
3102                c"test.xml".as_ptr() as *const c_char,
3103                ptr::null(),
3104                0,
3105            )
3106        };
3107        assert!(!doc.is_null());
3108
3109        let mut ctxt = RelaxNgValidCtxt::new();
3110        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3111        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3112
3113        assert!(
3114            valid,
3115            "Optional present validation failed: {:?}",
3116            ctxt.errors
3117        );
3118    }
3119
3120    /// Validates an `optional` element that is absent.
3121    ///
3122    /// # Safety
3123    ///
3124    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3125    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3126    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3127    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3128    #[test]
3129    fn test_validate_optional_absent() {
3130        let schema_xml = r#"<?xml version="1.0"?>
3131<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
3132  <optional>
3133    <element name="opt">
3134      <text/>
3135    </element>
3136  </optional>
3137  <text/>
3138</element>"#;
3139
3140        let doc_xml = r#"<?xml version="1.0"?>
3141<root>text only</root>"#;
3142
3143        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3144
3145        let doc = unsafe {
3146            crate::abi::exports_xml2::xmlReadMemory(
3147                doc_xml.as_ptr() as *const c_char,
3148                doc_xml.len() as c_int,
3149                c"test.xml".as_ptr() as *const c_char,
3150                ptr::null(),
3151                0,
3152            )
3153        };
3154        assert!(!doc.is_null());
3155
3156        let mut ctxt = RelaxNgValidCtxt::new();
3157        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3158        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3159
3160        assert!(
3161            valid,
3162            "Optional absent validation failed: {:?}",
3163            ctxt.errors
3164        );
3165    }
3166
3167    /// Validates children in the declared `sequence` order.
3168    ///
3169    /// # Safety
3170    ///
3171    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3172    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3173    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3174    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3175    #[test]
3176    fn test_validate_sequence() {
3177        let schema_xml = r#"<?xml version="1.0"?>
3178<grammar xmlns="http://relaxng.org/ns/structure/1.0">
3179  <start>
3180    <element name="root">
3181      <sequence>
3182        <element name="first">
3183          <text/>
3184        </element>
3185        <element name="second">
3186          <text/>
3187        </element>
3188      </sequence>
3189    </element>
3190  </start>
3191</grammar>"#;
3192
3193        let doc_xml = r#"<?xml version="1.0"?>
3194<root><first>First</first><second>Second</second></root>"#;
3195
3196        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3197
3198        let doc = unsafe {
3199            crate::abi::exports_xml2::xmlReadMemory(
3200                doc_xml.as_ptr() as *const c_char,
3201                doc_xml.len() as c_int,
3202                c"test.xml".as_ptr() as *const c_char,
3203                ptr::null(),
3204                0,
3205            )
3206        };
3207        assert!(!doc.is_null());
3208
3209        let mut ctxt = RelaxNgValidCtxt::new();
3210        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3211        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3212
3213        assert!(valid, "Sequence validation failed: {:?}", ctxt.errors);
3214    }
3215
3216    /// Validates a `data` pattern with an integer type.
3217    ///
3218    /// # Safety
3219    ///
3220    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3221    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3222    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3223    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3224    #[test]
3225    fn test_validate_data_pattern() {
3226        let schema_xml = r#"<?xml version="1.0"?>
3227<element name="age" xmlns="http://relaxng.org/ns/structure/1.0">
3228  <data type="integer"/>
3229</element>"#;
3230
3231        let doc_xml = r#"<?xml version="1.0"?>
3232<age>25</age>"#;
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, "Data pattern validation failed: {:?}", ctxt.errors);
3252    }
3253
3254    /// Verifies a non-integer value fails a `data` pattern.
3255    ///
3256    /// # Safety
3257    ///
3258    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3259    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3260    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3261    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3262    #[test]
3263    fn test_validate_data_pattern_invalid() {
3264        let schema_xml = r#"<?xml version="1.0"?>
3265<element name="age" xmlns="http://relaxng.org/ns/structure/1.0">
3266  <data type="integer"/>
3267</element>"#;
3268
3269        let doc_xml = r#"<?xml version="1.0"?>
3270<age>not-a-number</age>"#;
3271
3272        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3273
3274        let doc = unsafe {
3275            crate::abi::exports_xml2::xmlReadMemory(
3276                doc_xml.as_ptr() as *const c_char,
3277                doc_xml.len() as c_int,
3278                c"test.xml".as_ptr() as *const c_char,
3279                ptr::null(),
3280                0,
3281            )
3282        };
3283        assert!(!doc.is_null());
3284
3285        let mut ctxt = RelaxNgValidCtxt::new();
3286        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3287        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3288
3289        assert!(!valid, "Validation should have failed for invalid integer");
3290    }
3291
3292    /// Validates a `value` pattern with an exact match.
3293    ///
3294    /// # Safety
3295    ///
3296    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3297    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3298    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3299    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3300    #[test]
3301    fn test_validate_value_pattern() {
3302        let schema_xml = r#"<?xml version="1.0"?>
3303<element name="status" xmlns="http://relaxng.org/ns/structure/1.0">
3304  <value>active</value>
3305</element>"#;
3306
3307        let doc_xml = r#"<?xml version="1.0"?>
3308<status>active</status>"#;
3309
3310        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3311
3312        let doc = unsafe {
3313            crate::abi::exports_xml2::xmlReadMemory(
3314                doc_xml.as_ptr() as *const c_char,
3315                doc_xml.len() as c_int,
3316                c"test.xml".as_ptr() as *const c_char,
3317                ptr::null(),
3318                0,
3319            )
3320        };
3321        assert!(!doc.is_null());
3322
3323        let mut ctxt = RelaxNgValidCtxt::new();
3324        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3325        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3326
3327        assert!(valid, "Value pattern validation failed: {:?}", ctxt.errors);
3328    }
3329
3330    /// Verifies a `value` pattern mismatch fails validation.
3331    ///
3332    /// # Safety
3333    ///
3334    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3335    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3336    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3337    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3338    #[test]
3339    fn test_validate_value_pattern_mismatch() {
3340        let schema_xml = r#"<?xml version="1.0"?>
3341<element name="status" xmlns="http://relaxng.org/ns/structure/1.0">
3342  <value>active</value>
3343</element>"#;
3344
3345        let doc_xml = r#"<?xml version="1.0"?>
3346<status>inactive</status>"#;
3347
3348        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3349
3350        let doc = unsafe {
3351            crate::abi::exports_xml2::xmlReadMemory(
3352                doc_xml.as_ptr() as *const c_char,
3353                doc_xml.len() as c_int,
3354                c"test.xml".as_ptr() as *const c_char,
3355                ptr::null(),
3356                0,
3357            )
3358        };
3359        assert!(!doc.is_null());
3360
3361        let mut ctxt = RelaxNgValidCtxt::new();
3362        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3363        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3364
3365        assert!(!valid, "Validation should have failed for value mismatch");
3366    }
3367
3368    /// Verifies a `notAllowed` pattern always fails validation.
3369    ///
3370    /// # Safety
3371    ///
3372    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3373    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3374    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3375    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3376    #[test]
3377    fn test_validate_not_allowed() {
3378        let schema_xml = r#"<?xml version="1.0"?>
3379<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
3380  <notAllowed/>
3381</element>"#;
3382
3383        let doc_xml = r#"<?xml version="1.0"?>
3384<root>should not be allowed</root>"#;
3385
3386        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3387
3388        let doc = unsafe {
3389            crate::abi::exports_xml2::xmlReadMemory(
3390                doc_xml.as_ptr() as *const c_char,
3391                doc_xml.len() as c_int,
3392                c"test.xml".as_ptr() as *const c_char,
3393                ptr::null(),
3394                0,
3395            )
3396        };
3397        assert!(!doc.is_null());
3398
3399        let mut ctxt = RelaxNgValidCtxt::new();
3400        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3401        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3402
3403        assert!(!valid, "notAllowed should cause validation failure");
3404    }
3405
3406    /// Validates children in any order under an `interleave` pattern.
3407    ///
3408    /// # Safety
3409    ///
3410    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3411    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3412    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3413    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3414    #[test]
3415    fn test_validate_interleave() {
3416        let schema_xml = r#"<?xml version="1.0"?>
3417<grammar xmlns="http://relaxng.org/ns/structure/1.0">
3418  <start>
3419    <element name="root">
3420      <interleave>
3421        <element name="a">
3422          <text/>
3423        </element>
3424        <element name="b">
3425          <text/>
3426        </element>
3427      </interleave>
3428    </element>
3429  </start>
3430</grammar>"#;
3431
3432        let doc_xml = r#"<?xml version="1.0"?>
3433<root><a>A</a><b>B</b></root>"#;
3434
3435        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3436
3437        let doc = unsafe {
3438            crate::abi::exports_xml2::xmlReadMemory(
3439                doc_xml.as_ptr() as *const c_char,
3440                doc_xml.len() as c_int,
3441                c"test.xml".as_ptr() as *const c_char,
3442                ptr::null(),
3443                0,
3444            )
3445        };
3446        assert!(!doc.is_null());
3447
3448        let mut ctxt = RelaxNgValidCtxt::new();
3449        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3450        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3451
3452        assert!(valid, "Interleave validation failed: {:?}", ctxt.errors);
3453    }
3454
3455    /// Validates an empty element against an `empty` pattern.
3456    ///
3457    /// # Safety
3458    ///
3459    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3460    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3461    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3462    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3463    #[test]
3464    fn test_validate_empty_element() {
3465        let schema_xml = r#"<?xml version="1.0"?>
3466<element name="br" xmlns="http://relaxng.org/ns/structure/1.0">
3467  <empty/>
3468</element>"#;
3469
3470        let doc_xml = r#"<?xml version="1.0"?>
3471<br/>"#;
3472
3473        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3474
3475        let doc = unsafe {
3476            crate::abi::exports_xml2::xmlReadMemory(
3477                doc_xml.as_ptr() as *const c_char,
3478                doc_xml.len() as c_int,
3479                c"test.xml".as_ptr() as *const c_char,
3480                ptr::null(),
3481                0,
3482            )
3483        };
3484        assert!(!doc.is_null());
3485
3486        let mut ctxt = RelaxNgValidCtxt::new();
3487        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3488        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3489
3490        assert!(valid, "Empty element validation failed: {:?}", ctxt.errors);
3491    }
3492
3493    // ── Datatype Validation Tests ──────────────────────────────────────────
3494
3495    #[test]
3496    fn test_validate_datatype_string() {
3497        assert!(rng_validate_datatype_value(Some("string"), "hello"));
3498        assert!(rng_validate_datatype_value(Some("string"), ""));
3499    }
3500
3501    #[test]
3502    fn test_validate_datatype_boolean() {
3503        assert!(rng_validate_datatype_value(Some("boolean"), "true"));
3504        assert!(rng_validate_datatype_value(Some("boolean"), "false"));
3505        assert!(rng_validate_datatype_value(Some("boolean"), "1"));
3506        assert!(rng_validate_datatype_value(Some("boolean"), "0"));
3507        assert!(!rng_validate_datatype_value(Some("boolean"), "yes"));
3508        assert!(!rng_validate_datatype_value(Some("boolean"), "no"));
3509    }
3510
3511    #[test]
3512    fn test_validate_datatype_integer() {
3513        assert!(rng_validate_datatype_value(Some("integer"), "42"));
3514        assert!(rng_validate_datatype_value(Some("integer"), "-42"));
3515        assert!(rng_validate_datatype_value(Some("integer"), "+42"));
3516        assert!(!rng_validate_datatype_value(Some("integer"), "12.5"));
3517        assert!(!rng_validate_datatype_value(Some("integer"), "abc"));
3518        assert!(!rng_validate_datatype_value(Some("integer"), ""));
3519    }
3520
3521    #[test]
3522    fn test_validate_datatype_decimal() {
3523        assert!(rng_validate_datatype_value(Some("decimal"), "42"));
3524        assert!(rng_validate_datatype_value(Some("decimal"), "12.5"));
3525        assert!(rng_validate_datatype_value(Some("decimal"), "-3.14"));
3526        assert!(!rng_validate_datatype_value(Some("decimal"), ""));
3527    }
3528
3529    #[test]
3530    fn test_validate_datatype_float() {
3531        assert!(rng_validate_datatype_value(Some("float"), "3.14"));
3532        assert!(rng_validate_datatype_value(Some("float"), "INF"));
3533        assert!(rng_validate_datatype_value(Some("float"), "-INF"));
3534        assert!(rng_validate_datatype_value(Some("float"), "NaN"));
3535        assert!(!rng_validate_datatype_value(Some("float"), ""));
3536    }
3537
3538    #[test]
3539    fn test_validate_datatype_ncname() {
3540        assert!(rng_validate_datatype_value(Some("NCName"), "myElement"));
3541        assert!(rng_validate_datatype_value(Some("NCName"), "_foo"));
3542        assert!(!rng_validate_datatype_value(Some("NCName"), "123abc"));
3543        assert!(!rng_validate_datatype_value(Some("NCName"), ""));
3544    }
3545
3546    #[test]
3547    fn test_validate_datatype_any_uri() {
3548        assert!(rng_validate_datatype_value(
3549            Some("anyURI"),
3550            "http://example.com"
3551        ));
3552        assert!(rng_validate_datatype_value(Some("anyURI"), "urn:isbn:1234"));
3553        assert!(!rng_validate_datatype_value(Some("anyURI"), ""));
3554        assert!(!rng_validate_datatype_value(Some("anyURI"), "has space"));
3555    }
3556
3557    #[test]
3558    fn test_validate_datatype_qname() {
3559        assert!(rng_validate_datatype_value(Some("QName"), "ns:local"));
3560        assert!(rng_validate_datatype_value(Some("QName"), "local"));
3561        assert!(!rng_validate_datatype_value(Some("QName"), ""));
3562    }
3563
3564    // ── C ABI Tests ───────────────────────────────────────────────────────
3565
3566    /// Exercises the C ABI schema parse round trip: `xmlRelaxNGNewMemParserCtxt`,
3567    /// `xmlRelaxNGParse` and `xmlRelaxNGFree`.
3568    ///
3569    /// # Safety
3570    ///
3571    /// - The static `schema_xml` string stays valid for the
3572    ///   `xmlRelaxNGNewMemParserCtxt` call; the parser context and the schema
3573    ///   returned by `xmlRelaxNGParse` are asserted non-NULL before use, and
3574    ///   the schema is freed exactly once with `xmlRelaxNGFree`.
3575    #[test]
3576    fn test_c_abi_new_parse_free() {
3577        let schema_xml = r#"<?xml version="1.0"?>
3578<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
3579  <text/>
3580</element>"#;
3581
3582        let ctxt = unsafe {
3583            xmlRelaxNGNewMemParserCtxt(
3584                schema_xml.as_ptr() as *const c_char,
3585                schema_xml.len() as c_int,
3586            )
3587        };
3588        assert!(!ctxt.is_null(), "Parser context should not be null");
3589
3590        let schema = unsafe { xmlRelaxNGParse(ctxt) };
3591        assert!(!schema.is_null(), "Schema should not be null");
3592
3593        // Free the schema
3594        unsafe { xmlRelaxNGFree(schema) };
3595    }
3596
3597    /// Exercises the C ABI document validation path.
3598    ///
3599    /// # Safety
3600    ///
3601    /// - The static schema and document strings stay valid for their
3602    ///   `xmlRelaxNGNewMemParserCtxt` and `xmlReadMemory` calls; the parser
3603    ///   context, schema, validation context and document pointers are
3604    ///   asserted non-NULL before use, and the document, validation context
3605    ///   and schema are each freed exactly once.
3606    #[test]
3607    fn test_c_abi_validate_doc() {
3608        let schema_xml = r#"<?xml version="1.0"?>
3609<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
3610  <text/>
3611</element>"#;
3612
3613        let doc_xml = r#"<?xml version="1.0"?>
3614<root>Hello</root>"#;
3615
3616        let ctxt = unsafe {
3617            xmlRelaxNGNewMemParserCtxt(
3618                schema_xml.as_ptr() as *const c_char,
3619                schema_xml.len() as c_int,
3620            )
3621        };
3622        let schema = unsafe { xmlRelaxNGParse(ctxt) };
3623        assert!(!schema.is_null());
3624
3625        let valid_ctxt = unsafe { xmlRelaxNGNewValidCtxt(schema) };
3626        assert!(!valid_ctxt.is_null());
3627
3628        let doc = unsafe {
3629            crate::abi::exports_xml2::xmlReadMemory(
3630                doc_xml.as_ptr() as *const c_char,
3631                doc_xml.len() as c_int,
3632                c"test.xml".as_ptr() as *const c_char,
3633                ptr::null(),
3634                0,
3635            )
3636        };
3637        assert!(!doc.is_null());
3638
3639        let result = unsafe { xmlRelaxNGValidateDoc(valid_ctxt, doc) };
3640        assert_eq!(result, 0, "Validation should succeed");
3641
3642        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3643        unsafe { xmlRelaxNGFreeValidCtxt(valid_ctxt) };
3644        unsafe { xmlRelaxNGFree(schema) };
3645    }
3646
3647    /// Exercises the C ABI full-element validation path.
3648    ///
3649    /// # Safety
3650    ///
3651    /// - The static schema and document strings stay valid for their
3652    ///   `xmlRelaxNGNewMemParserCtxt` and `xmlReadMemory` calls; the parser
3653    ///   context, schema, validation context and document pointers are
3654    ///   asserted non-NULL before use.
3655    /// - The document tree walked through `children` and `next` links must be
3656    ///   well-formed and NULL-terminated; the located `item` node is asserted
3657    ///   non-NULL before being passed to `xmlRelaxNGValidateFullElement`.
3658    /// - The document, validation context and schema are each freed exactly
3659    ///   once.
3660    #[test]
3661    fn test_c_abi_validate_full_element() {
3662        let schema_xml = r#"<?xml version="1.0"?>
3663<element name="item" xmlns="http://relaxng.org/ns/structure/1.0">
3664  <text/>
3665</element>"#;
3666
3667        let doc_xml = r#"<?xml version="1.0"?>
3668<root><item>Content</item></root>"#;
3669
3670        let ctxt = unsafe {
3671            xmlRelaxNGNewMemParserCtxt(
3672                schema_xml.as_ptr() as *const c_char,
3673                schema_xml.len() as c_int,
3674            )
3675        };
3676        let schema = unsafe { xmlRelaxNGParse(ctxt) };
3677        assert!(!schema.is_null());
3678
3679        let valid_ctxt = unsafe { xmlRelaxNGNewValidCtxt(schema) };
3680        assert!(!valid_ctxt.is_null());
3681
3682        let doc = unsafe {
3683            crate::abi::exports_xml2::xmlReadMemory(
3684                doc_xml.as_ptr() as *const c_char,
3685                doc_xml.len() as c_int,
3686                c"test.xml".as_ptr() as *const c_char,
3687                ptr::null(),
3688                0,
3689            )
3690        };
3691        assert!(!doc.is_null());
3692
3693        // Find the <item> element
3694        let item = unsafe {
3695            // Start from the first child of the document (root element)
3696            let mut node = (*doc).children;
3697            while !node.is_null() {
3698                if (*node).type_ == XML_ELEMENT_NODE as c_int {
3699                    break;
3700                }
3701                node = (*node).next;
3702            }
3703            if !node.is_null() {
3704                // Now find <item> child of root
3705                node = (*node).children;
3706                while !node.is_null() {
3707                    if (*node).type_ == XML_ELEMENT_NODE as c_int {
3708                        break;
3709                    }
3710                    node = (*node).next;
3711                }
3712            }
3713            node
3714        };
3715        assert!(!item.is_null(), "Should find <item> element");
3716
3717        let result = unsafe { xmlRelaxNGValidateFullElement(valid_ctxt, doc, item) };
3718        assert_eq!(result, 0, "Element validation should succeed");
3719
3720        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3721        unsafe { xmlRelaxNGFreeValidCtxt(valid_ctxt) };
3722        unsafe { xmlRelaxNGFree(schema) };
3723    }
3724
3725    /// Verifies the C ABI accepts NULL pointers and NULL frees.
3726    ///
3727    /// # Safety
3728    ///
3729    /// - NULL pointers are passed only to C ABI entry points that accept NULL
3730    ///   inputs, and the free functions must tolerate NULL without
3731    ///   dereferencing.
3732    #[test]
3733    fn test_c_abi_null_handling() {
3734        // Test null pointer handling
3735        assert_eq!(
3736            unsafe { xmlRelaxNGValidateDoc(ptr::null_mut(), ptr::null_mut()) },
3737            -1
3738        );
3739        assert_eq!(
3740            unsafe {
3741                xmlRelaxNGValidateFullElement(ptr::null_mut(), ptr::null_mut(), ptr::null_mut())
3742            },
3743            -1
3744        );
3745        assert!(unsafe { xmlRelaxNGNewMemParserCtxt(ptr::null(), 0).is_null() });
3746
3747        // Free with null should not crash
3748        unsafe { xmlRelaxNGFree(ptr::null_mut()) };
3749        unsafe { xmlRelaxNGFreeParserCtxt(ptr::null_mut()) };
3750        unsafe { xmlRelaxNGFreeValidCtxt(ptr::null_mut()) };
3751    }
3752
3753    // ── Edge Case Tests ───────────────────────────────────────────────────
3754
3755    #[test]
3756    fn test_parse_with_div() {
3757        let schema_xml = r#"<?xml version="1.0"?>
3758<grammar xmlns="http://relaxng.org/ns/structure/1.0">
3759  <div>
3760    <define name="shared">
3761      <text/>
3762    </define>
3763  </div>
3764  <start>
3765    <element name="root">
3766      <ref name="shared"/>
3767    </element>
3768  </start>
3769</grammar>"#;
3770
3771        let result = rng_parse(schema_xml);
3772        assert!(
3773            result.is_ok(),
3774            "Failed to parse with div: {:?}",
3775            result.err()
3776        );
3777        let schema = result.unwrap();
3778        assert_eq!(schema.grammar.defines.len(), 1);
3779        assert_eq!(schema.grammar.defines[0].name, "shared");
3780    }
3781
3782    /// Validates a `list` pattern with space-separated tokens.
3783    ///
3784    /// # Safety
3785    ///
3786    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3787    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3788    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3789    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3790    #[test]
3791    fn test_validate_list_pattern() {
3792        let schema_xml = r#"<?xml version="1.0"?>
3793<element name="tokens" xmlns="http://relaxng.org/ns/structure/1.0">
3794  <list>
3795    <data type="token"/>
3796  </list>
3797</element>"#;
3798
3799        let doc_xml = r#"<?xml version="1.0"?>
3800<tokens>abc def ghi</tokens>"#;
3801
3802        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3803
3804        let doc = unsafe {
3805            crate::abi::exports_xml2::xmlReadMemory(
3806                doc_xml.as_ptr() as *const c_char,
3807                doc_xml.len() as c_int,
3808                c"test.xml".as_ptr() as *const c_char,
3809                ptr::null(),
3810                0,
3811            )
3812        };
3813        assert!(!doc.is_null());
3814
3815        let mut ctxt = RelaxNgValidCtxt::new();
3816        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3817        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3818
3819        assert!(valid, "List pattern validation failed: {:?}", ctxt.errors);
3820    }
3821
3822    /// Validates children in order under a `group` pattern.
3823    ///
3824    /// # Safety
3825    ///
3826    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3827    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3828    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3829    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3830    #[test]
3831    fn test_validate_group_pattern() {
3832        let schema_xml = r#"<?xml version="1.0"?>
3833<grammar xmlns="http://relaxng.org/ns/structure/1.0">
3834  <start>
3835    <element name="root">
3836      <group>
3837        <element name="a">
3838          <text/>
3839        </element>
3840        <element name="b">
3841          <text/>
3842        </element>
3843      </group>
3844    </element>
3845  </start>
3846</grammar>"#;
3847
3848        let doc_xml = r#"<?xml version="1.0"?>
3849<root><a>First</a><b>Second</b></root>"#;
3850
3851        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3852
3853        let doc = unsafe {
3854            crate::abi::exports_xml2::xmlReadMemory(
3855                doc_xml.as_ptr() as *const c_char,
3856                doc_xml.len() as c_int,
3857                c"test.xml".as_ptr() as *const c_char,
3858                ptr::null(),
3859                0,
3860            )
3861        };
3862        assert!(!doc.is_null());
3863
3864        let mut ctxt = RelaxNgValidCtxt::new();
3865        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3866        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3867
3868        assert!(valid, "Group validation failed: {:?}", ctxt.errors);
3869    }
3870
3871    /// Verifies validation fails when a `ref` names an undefined define.
3872    ///
3873    /// # Safety
3874    ///
3875    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3876    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3877    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3878    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3879    #[test]
3880    fn test_validate_undefined_ref() {
3881        let schema_xml = r#"<?xml version="1.0"?>
3882<grammar xmlns="http://relaxng.org/ns/structure/1.0">
3883  <start>
3884    <element name="root">
3885      <ref name="undefined"/>
3886    </element>
3887  </start>
3888</grammar>"#;
3889
3890        let doc_xml = r#"<?xml version="1.0"?>
3891<root>Content</root>"#;
3892
3893        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3894
3895        let doc = unsafe {
3896            crate::abi::exports_xml2::xmlReadMemory(
3897                doc_xml.as_ptr() as *const c_char,
3898                doc_xml.len() as c_int,
3899                c"test.xml".as_ptr() as *const c_char,
3900                ptr::null(),
3901                0,
3902            )
3903        };
3904        assert!(!doc.is_null());
3905
3906        let mut ctxt = RelaxNgValidCtxt::new();
3907        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3908        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3909
3910        assert!(!valid, "Undefined ref should cause failure");
3911    }
3912
3913    /// Verifies `rng_validate_doc` rejects a NULL document pointer.
3914    ///
3915    /// # Safety
3916    ///
3917    /// - `rng_validate_doc` is called with a NULL document pointer, which it
3918    ///   must reject without dereferencing.
3919    #[test]
3920    fn test_validate_null_doc() {
3921        let schema = RelaxNgSchema::new();
3922        let mut ctxt = RelaxNgValidCtxt::new();
3923        let valid = unsafe { rng_validate_doc(&schema, ptr::null_mut(), &mut ctxt) };
3924        assert!(!valid);
3925    }
3926
3927    #[test]
3928    fn test_parse_external_ref_schema() {
3929        let schema_xml = r#"<?xml version="1.0"?>
3930<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
3931  <externalRef href="external.rng"/>
3932</element>"#;
3933
3934        let result = rng_parse(schema_xml);
3935        assert!(
3936            result.is_ok(),
3937            "Failed to parse externalRef: {:?}",
3938            result.err()
3939        );
3940        let schema = result.unwrap();
3941        if let Some(ref start) = schema.grammar.start {
3942            assert_eq!(start.pattern_type, RelaxNgPatternType::Element);
3943            assert_eq!(start.name.as_deref(), Some("root"));
3944        }
3945    }
3946
3947    #[test]
3948    fn test_validate_boolean_datatype() {
3949        assert!(rng_validate_datatype_value(Some("boolean"), "true"));
3950        assert!(rng_validate_datatype_value(Some("boolean"), "false"));
3951        assert!(!rng_validate_datatype_value(Some("boolean"), "maybe"));
3952    }
3953
3954    #[test]
3955    fn test_validate_unknown_datatype() {
3956        // Unknown datatypes should be accepted (lenient behavior)
3957        assert!(rng_validate_datatype_value(Some("custom-type"), "anything"));
3958    }
3959
3960    #[test]
3961    fn test_validate_no_datatype() {
3962        // No datatype specified — accept anything
3963        assert!(rng_validate_datatype_value(None, "anything"));
3964    }
3965
3966    #[test]
3967    fn test_parse_schema_with_ns_prefix() {
3968        let schema_xml = r#"<?xml version="1.0"?>
3969<rng:element name="root" xmlns:rng="http://relaxng.org/ns/structure/1.0">
3970  <rng:text/>
3971</rng:element>"#;
3972
3973        let result = rng_parse(schema_xml);
3974        assert!(result.is_ok(), "Failed with ns prefix: {:?}", result.err());
3975    }
3976
3977    #[test]
3978    fn test_validation_context_path() {
3979        let mut ctxt = RelaxNgValidCtxt::new();
3980        assert_eq!(ctxt.current_path(), "/");
3981
3982        ctxt.path.push("root".to_string());
3983        assert_eq!(ctxt.current_path(), "/root");
3984
3985        ctxt.path.push("child".to_string());
3986        assert_eq!(ctxt.current_path(), "/root/child");
3987
3988        ctxt.path.pop();
3989        assert_eq!(ctxt.current_path(), "/root");
3990    }
3991
3992    /// Verifies a `sequence` pattern rejects children in the wrong order.
3993    ///
3994    /// # Safety
3995    ///
3996    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3997    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3998    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3999    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
4000    #[test]
4001    fn test_validate_sequence_wrong_order() {
4002        let schema_xml = r#"<?xml version="1.0"?>
4003<grammar xmlns="http://relaxng.org/ns/structure/1.0">
4004  <start>
4005    <element name="root">
4006      <sequence>
4007        <element name="first">
4008          <text/>
4009        </element>
4010        <element name="second">
4011          <text/>
4012        </element>
4013      </sequence>
4014    </element>
4015  </start>
4016</grammar>"#;
4017
4018        let doc_xml = r#"<?xml version="1.0"?>
4019<root><second>Wrong</second><first>Order</first></root>"#;
4020
4021        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
4022
4023        let doc = unsafe {
4024            crate::abi::exports_xml2::xmlReadMemory(
4025                doc_xml.as_ptr() as *const c_char,
4026                doc_xml.len() as c_int,
4027                c"test.xml".as_ptr() as *const c_char,
4028                ptr::null(),
4029                0,
4030            )
4031        };
4032        assert!(!doc.is_null());
4033
4034        let mut ctxt = RelaxNgValidCtxt::new();
4035        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
4036        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
4037
4038        // The sequence validates patterns against children in order,
4039        // so wrong order should fail
4040        assert!(!valid, "Wrong sequence order should fail");
4041    }
4042}