Skip to main content

libxml_rs/xml/relaxng/
mod.rs

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