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