Skip to main content

libxml_rs/xml/relaxng/
mod.rs

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